-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathSesh.cs
More file actions
611 lines (535 loc) · 20.3 KB
/
Sesh.cs
File metadata and controls
611 lines (535 loc) · 20.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
namespace SKX
{
/// <summary>
/// Represents a game session; stored on disk as a saved game, but also holds
/// other data that persists between levels.
/// </summary>
public class Sesh
{
/// <summary>
/// The number of the save slot (arbitrary)
/// </summary>
public int SaveSlot { get; set; } = 1;
/// <summary>
/// The story this game is using
/// </summary>
public Story Story { get; set; } = Story.Classic;
/// <summary>
/// Starting difficulty
/// </summary>
public Difficulty Difficulty { get; set; } = Difficulty.Normal;
/* Dana/Adam's Properties */
/// <summary>
/// Whether or not Dana (false) or Adam (true) is currently selected
/// </summary>
public bool Apprentice { get; set; } = false; // Adam or Dana?
public int DanaLives { get; set; } = 3;
public int AdamLives { get; set; } = 3;
public int Score { get; set; } = 0;
public int LastShrine { get; set; } = 0;
public int Fairies { get; set; }
public int RoomNumber { get; set; } = 1;
public int RoomAttempt { get; set; } = 0;
/// <summary>
/// List of inventory items obtained
/// </summary>
public List<InventoryItem> Inventory { get; set; } = new List<InventoryItem>();
/// <summary>
/// Global list of all spells executed
/// </summary>
public List<SpellExecuted> SpellsExecuted { get; set; } = new List<SpellExecuted>();
public DateTime SaveTime { get; set; }
public bool ScrollDisabled { get; set; } = false;
/// <summary>
/// This flag (set via Spell) causes a normal exit door
/// to use the "next secret room number" as its destination
/// </summary>
public bool SecretExit { get; set; } = false;
public int FireballRange { get; set; } = Game.MinFireballRange;
/// <summary>
/// How many simultaneous fireballs can be launched
/// </summary>
public int MaxFireballs { get; set; } = 1;
internal bool NoPoints = false; // Don't tally Dana's life on the next
// thank you dana screen
/* Characters' Scrolls */
public int DanaScrollSize { get; set; } = 3;
public int AdamScrollSize { get; set; } = 3;
public List<Cell> DanaScroll { get; } = new List<Cell>();
public List<Cell> AdamScroll { get; } = new List<Cell>();
/* GDV Stats (both characters) */
public int PickupCount { get; set; }
public int KillCount { get; set; }
public int RoomsCleared { get; set; }
public int HardRoomsCleared { get; set; }
public Progress Progress { get; set; }
public int TotalFairies { get; set; }
/* Things used between rooms */
/// <summary>
/// Used internally to determine if a demo is playing back organically
/// (from the title screen).
/// </summary>
[JsonIgnore] public bool DemoPlayback { get; set; }
public bool FastStars { get; set; }
public bool SkipThankYou { get; set; }
public Point WarpTo { get; set; }
public int ReturnToRoom { get; set; } = 0; // Used after a "hidden"
public Point ReturnToWarpTo { get; set; } = default; // Used after a "hidden"
public bool BonusRoomQueued { get; set; } = false; // Whether to do a hidden after next room
public List<OpenDoor> DoorsOpened { get; set; } = new List<OpenDoor>(); // Doors opened in the current room
/* Things stored for the level editor */
public bool EditorCameraLocked { get; set; } = true;
public Cell EditorCell { get; set; }
public EditMode LastEditMode { get; set; } = EditMode.Layout;
/* Computed properties */
[JsonIgnore]
public string PlayerName => Apprentice ? "ADAM" : "DANA";
[JsonIgnore]
public List<Cell> ScrollItems => Apprentice ? AdamScroll : DanaScroll;
[JsonIgnore]
public int ScrollSize
{
get => Apprentice ? AdamScrollSize : DanaScrollSize;
set { if (Apprentice) AdamScrollSize = value; else DanaScrollSize = value; }
}
/// <summary>
/// Gets or sets how many lives the current character has
/// </summary>
public int Lives
{
get => Apprentice ? AdamLives : DanaLives;
set
{
if (Apprentice) AdamLives = value; else DanaLives = value;
}
}
/* Constructors */
public Sesh() { }
public Sesh(Difficulty difficulty, Story story, int slot)
{
Game.LogInfo($"Session created ({difficulty}, {story}, slot {slot})");
Difficulty = difficulty;
Story = story;
SaveSlot = slot;
RoomNumber = 1;
if (story == Story.Test) RoomNumber = 0;
switch (difficulty)
{
case Difficulty.Normal:
break;
case Difficulty.Easy:
Lives = 5;
FireballRange = 20;
ScrollSize = 5;
break;
case Difficulty.Hard:
Lives = 3;
ScrollSize = 1;
break;
}
}
/// <summary>
/// Clears anything non-persistent from the current rooms inventory
/// </summary>
public void ClearTempInventory()
{
Inventory.RemoveAll(x => !Layout.IsPersistent(x.Type) && x.FromRoom == RoomNumber);
}
/// <summary>
/// Loads a room layout for the current game type, room, etc.
/// </summary>
private Layout BuildLayout()
{
Layout l;
Game.LogInfo($"Building layout for room {RoomNumber:X}{Story.ToStoryID()}");
// 1. Look to see if a room file exists. Use this instead of the
// internal room if it exists
var file = $"room_{RoomNumber:X}{Story.ToStoryID()}.json";
if (file.FileExists())
{
Game.LogInfo($"Loading room {RoomNumber:X}{Story.ToStoryID()} from {file}");
return Layout.LoadFile(file, null);
}
// 2. Attempt to load the room from the embedded bundle
if (Game.Assets.Bundle != null)
{
l = Game.Assets.Bundle.LoadRoom(Story, RoomNumber);
Game.LogInfo($"Loaded room {RoomNumber:X}{Story.ToStoryID()} from bundle");
if (l != null) return l;
}
// 3. If the ROM is available import the level from the ROM
file = @"c:\nes\sk02.nes";
var hex = RoomNumber.ToString("X");
if (!int.TryParse(hex, out int rm))
{
rm = 0;
}
if (file.FileExists()) {
l = Layout.ImportLegacy(file, rm, null);
l.RoomNumber = RoomNumber;
Game.LogInfo($"Loaded room {RoomNumber:X}{Story.ToStoryID()} from legacy ROM");
return l;
}
// 4. Return a blank layout lol
Game.LogInfo($"Cannot find a layout for {RoomNumber:X}{Story.ToStoryID()}");
return Layout.BlankLayout(RoomNumber, null);
}
/// <summary>
/// Sets the room number for the hidden level and sets the "return to" room
/// to be the previous value
/// </summary>
public void SetUpHiddenRoom()
{
Game.LogInfo($"Entering hidden room");
ReturnToRoom = RoomNumber;
ReturnToWarpTo = WarpTo;
switch (Story)
{
case Story.Classic:
RoomNumber = 0;
break;
case Story.Plus:
RoomNumber = 0;
break;
case Story.SKX:
RoomNumber = 0x101;
break;
}
}
/// <summary>
/// Checks if a specific spell has executed (at any time during the game)
/// </summary>
public bool HasSpell(SpellExecuted spell)
{
if (spell.FromRoom == 0)
{
return SpellsExecuted.Any(s => s.SpellID == spell.SpellID);
}
else return SpellsExecuted.Contains(spell);
}
/// <summary>
/// Checks if Dana/Adam's inventory contains an item. Set the items FromRoom to 0 to ignore the room number
/// it was obtained in.
/// </summary>
public int InventoryContains(InventoryItem i)
{
if (i.FromRoom < 1)
{
return Inventory.Count(x => x.Type == i.Type);
} else
{
return Inventory.Count(x => x.Type == i.Type && x.FromRoom == i.FromRoom);
}
}
/// <summary>
/// Checks if a specific item type is in Dana's inventory for the current room
/// </summary>
public bool HasItemFromThisRoom(Cell c)
{
return Inventory.Any(x => x.Type == c && x.FromRoom == RoomNumber);
}
/// <summary>
/// Checks if a specific item is in Dana's inventory for a specific room
/// </summary>
public bool HasItem(Cell c, int roomNumber)
{
return Inventory.Any(x => x.Type == c && x.FromRoom == roomNumber);
}
/// <summary>
/// Checks if a specific item is in Dana's inventory (any room)
/// </summary>
public bool HasItem(Cell c)
{
return Inventory.Any(x => x.Type == c);
}
/// <summary>
/// Gets an inventory item of a specific type for a specific room
/// </summary>
public InventoryItem GetItem(Cell c, int roomNumber)
{
return Inventory.FirstOrDefault(x => x.Type == c && x.FromRoom == roomNumber);
}
/// <summary>
/// Calculates Dana's GDV. Algorithm RE'ed by https://github.com/pellsson
///
/// Warning: If this becomes at all an expensive calculation
/// it should be cached someplace because right now it's being calculated on every frame
/// of the game over screen.
/// </summary>
public int CalculateGDV()
{
int specials = Inventory.Count(x => x.Type == Cell.PageSpace || x.Type == Cell.PageTime);
int seals = Inventory.Count(x => x.Type == Cell.Seal);
int saved_princess = Progress.HasFlag(Progress.SavedPrincess) ? 1 : 0;
int has_solomons_key = Progress.HasFlag(Progress.FoundSolomonsKey) ? 1 : 0;
var exp = ((1 + specials + (TotalFairies / 10) + saved_princess) * 2 + RoomsCleared + seals) * 2 + HardRoomsCleared;
return 47
+ has_solomons_key
+ exp / 8
+ Math.Min(5, Score / 100000);
}
/// <summary>
/// Used when the room changes
/// </summary>
public void OnNextLevel()
{
SecretExit = false;
RoomAttempt = 0;
Save();
}
/// <summary>
/// Used when the continue code occurs at a Game Over
/// </summary>
public void OnContinue()
{
AdamLives = 3;
DanaLives = 3;
DoorsOpened.RemoveAll(x => x.Room == RoomNumber);
if (Game.IsClassic && RoomNumber > 0x41)
{
Inventory.RemoveAll(x => x.FromRoom >= 0x41);
RoomNumber = 0x41;
} else
{
Inventory.RemoveAll(x => x.FromRoom == RoomNumber);
}
}
/// <summary>
/// Used after a player completes a level or dies, if the player was on a
/// hidden stage, they are returned to the next room they were going to go to
/// normally.
/// </summary>
public void CheckReturn()
{
if (ReturnToRoom > 0)
{
// Remove all objects obtained in the hidden
Inventory.RemoveAll(x => x.FromRoom == RoomNumber);
RoomNumber = ReturnToRoom;
WarpTo = ReturnToWarpTo;
ReturnToRoom = 0;
ReturnToWarpTo = default;
}
}
/// <summary>
/// Resets the layout for a specific room after a death or upon entering
/// the level editor
/// </summary>
public void ResetLayout(Level l)
{
var layout = BuildLayout();
l.Layout = layout;
l.Layout.World = l;
l.Layout.EnsureSize();
SecretExit = false;
}
/// <summary>
/// Checks if Dana has the shrine for the current room in his inventory.
/// </summary>
public bool HasThisRoomsShrine()
{
return Inventory.Any(x => x.FromRoom == RoomNumber
&& Layout.IsShrine(x.Type)); // Only constellation and planetary signs
}
/// <summary>
/// Increments the room tally metrics (used in GDV)
/// </summary>
public void AddRoomTally(int roomNum)
{
RoomsCleared++;
if (roomNum > 0x10 && ReturnToRoom == 0) HardRoomsCleared++;
}
/// <summary>
/// Builds a Level World based on the current room number
/// </summary>
public Level BuildLevel(Demo demo = null)
{
var layout = BuildLayout();
var l = new Level(layout.Width, layout.Height, demo);
l.Layout = layout;
l.Layout.World = l;
l.Layout.EnsureSize();
if (l.Layout.Shrine >= 0) LastShrine = l.Layout.Shrine;
return l;
}
/// <summary>
/// Builds a Level World based on a provided Layout
/// </summary>
public Level BuildLevel(Layout layout)
{
var l = new Level(layout.Width, layout.Height);
l.Layout = layout;
l.Layout.World = l;
l.Layout.EnsureSize();
if (l.Layout.Shrine >= 0) LastShrine = l.Layout.Shrine;
return l;
}
/// <summary>
/// Saves the game (to disk)
/// </summary>
public void Save()
{
SaveTime = DateTime.Now;
try
{
Game.SaveOptions(); // Save options, notably play timer
} catch (Exception ex)
{
Game.LogError($"Failed to save options: {ex}");
}
if (!this.SaveFile($"save_{SaveSlot}{Game.StoryID}.json"))
{
Game.LogError($"Failed to save to save_{SaveSlot}{Game.StoryID}.json");
Game.StatusMessage("ERROR SAVING GAME");
}
else
Game.LogInfo($"Session saved");
}
/// <summary>
/// Adds an item to Dana's inventory
/// </summary>
public void AddInventory(Cell type, Point fromCell, int? roomNumber = null)
{
roomNumber ??= RoomNumber;
Game.LogInfo($"Session: Added {type} to inventory (room {roomNumber})");
Inventory.Add(new InventoryItem()
{
FromRoom = roomNumber.Value,
Type = type,
FromCell = fromCell
});
}
/// <summary>
/// Builds the Load Game menu
/// </summary>
public static Menu BuildLoadGameMenu()
{
var m = new Menu("SELECT SAVED GAME");
var games = GetSavedGames();
if (games is null)
{
m.MenuItems.Add(new MenuItem("ERROR LOADING SAVE SLOTS"));
}
else if (games.Count() == 0)
{
m.MenuItems.Add(new MenuItem("NO SAVED GAMES FOUND"));
}
else
{
foreach(var g in games.OrderByDescending(x => x.SaveTime))
{
var mi = new MenuItem();
mi.Text = g.GetSlotText();
mi.Action = () =>
{
Game.Load(g);
};
m.MenuItems.Add(mi);
}
}
m.UpdateBounds();
return m;
}
/// <summary>
/// Gets all of the saved games in the game's directory
/// </summary>
public static IEnumerable<Sesh> GetSavedGames()
{
var l = new List<Sesh>();
try
{
var files = System.IO.Directory.GetFiles(Game.AppDirectory, "save*.json");
foreach(var f in files)
{
try
{
var s = f.LoadFile<Sesh>();
if (s != null) l.Add(s);
}
catch { continue; }
}
} catch (Exception ex)
{
Game.LogError($"Failed to enumerate save files: {ex}");
return null;
}
return l;
}
/// <summary>
/// Gets the text for the save slots in the Menu system
/// </summary>
public string GetSlotText()
{
var rm = ReturnToRoom > 0 ? ReturnToRoom : RoomNumber;
var name = Game.Assets.Bundle.GetRoomName(Story, rm) ?? $"ROOM {rm:X}";
return $"*{SaveSlot} {(Apprentice ? "a" : "d")}x{Lives} {Story.ToStoryID()} {name}";
}
/// <summary>
/// Gets the status of the specified save slot, used for the
/// main menu
/// </summary>
/// <returns>Returns null if the slot is empty</returns>
public string GetSlotStatus(int slot, Story story)
{
var file = $"save_{slot}{story.ToStoryID()}.json";
if (!file.FileExists()) return null;
try
{
var load = file.LoadFile<Sesh>();
if (load != null)
{
return load.GetSlotText();
}
} catch (Exception ex)
{
Game.LogError($"Failed to get save slot {slot} status: {ex}");
return "FILE ERROR";
}
return null;
}
}
/// <summary>
/// When Dana collects certain items they will go into his inventory
/// so that game logic later on can check for them
/// </summary>
public struct InventoryItem
{
public Cell Type;
public Point FromCell;
public int FromRoom;
public override string ToString()
{
return $"{FromRoom,2}: {Type.ToString().ToUpper()} {FromCell.X} {FromCell.Y}";
}
}
/// <summary>
/// When a spell is executed it goes into Dana's history so it can be checked by
/// game logic later on
/// </summary>
public struct SpellExecuted
{
public int FromRoom;
public int SpellID;
public override string ToString()
{
return $"{FromRoom,2}: SPELL {SpellID}";
}
}
/// <summary>
/// Progress flags used by the ending and GDV
/// </summary>
[Flags]
public enum Progress
{
None = 0,
SavedPrincess = 1,
FoundSolomonsKey = 2,
TrainedAdam = 4
}
}