isaacscript-common 6.16.2 → 6.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/enums/ModCallbackCustom.d.ts +1 -1
  2. package/dist/features/customGridEntity.d.ts +3 -3
  3. package/dist/features/customGridEntity.d.ts.map +1 -1
  4. package/dist/features/customGridEntity.lua +18 -11
  5. package/dist/features/extraConsoleCommands/listCommands.lua +2 -2
  6. package/dist/features/persistentEntities.d.ts.map +1 -1
  7. package/dist/features/persistentEntities.lua +15 -5
  8. package/dist/features/pickupIndex.d.ts.map +1 -1
  9. package/dist/features/pickupIndex.lua +5 -6
  10. package/dist/features/roomHistory.d.ts +11 -4
  11. package/dist/features/roomHistory.d.ts.map +1 -1
  12. package/dist/features/roomHistory.lua +18 -5
  13. package/dist/functions/log.d.ts +1 -1
  14. package/dist/functions/log.d.ts.map +1 -1
  15. package/dist/functions/log.lua +33 -28
  16. package/dist/functions/npcs.d.ts +9 -0
  17. package/dist/functions/npcs.d.ts.map +1 -1
  18. package/dist/functions/npcs.lua +16 -3
  19. package/dist/functions/playerHealth.d.ts.map +1 -1
  20. package/dist/functions/playerHealth.lua +29 -12
  21. package/dist/functions/rockAlt.d.ts +4 -4
  22. package/dist/functions/rockAlt.d.ts.map +1 -1
  23. package/dist/functions/rockAlt.lua +38 -87
  24. package/dist/functions/saveFile.d.ts +5 -0
  25. package/dist/functions/saveFile.d.ts.map +1 -1
  26. package/dist/functions/saveFile.lua +112 -3
  27. package/dist/index.d.ts +1 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.lua +1 -9
  30. package/dist/interfaces/{CustomGridEntityData.d.ts → GridEntityCustomData.d.ts} +2 -2
  31. package/dist/interfaces/{CustomGridEntityData.d.ts.map → GridEntityCustomData.d.ts.map} +1 -1
  32. package/dist/interfaces/{CustomGridEntityData.lua → GridEntityCustomData.lua} +0 -0
  33. package/dist/interfaces/PlayerHealth.d.ts +4 -1
  34. package/dist/interfaces/PlayerHealth.d.ts.map +1 -1
  35. package/dist/interfaces/RoomDescription.d.ts +1 -0
  36. package/dist/interfaces/RoomDescription.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/enums/ModCallbackCustom.ts +1 -1
  39. package/src/features/customGridEntity.ts +30 -17
  40. package/src/features/extraConsoleCommands/listCommands.ts +2 -2
  41. package/src/features/persistentEntities.ts +19 -5
  42. package/src/features/pickupIndex.ts +9 -15
  43. package/src/features/roomHistory.ts +24 -4
  44. package/src/functions/log.ts +35 -30
  45. package/src/functions/npcs.ts +27 -2
  46. package/src/functions/playerHealth.ts +25 -12
  47. package/src/functions/rockAlt.ts +50 -132
  48. package/src/functions/saveFile.ts +146 -4
  49. package/src/index.ts +1 -2
  50. package/src/interfaces/{CustomGridEntityData.ts → GridEntityCustomData.ts} +1 -1
  51. package/src/interfaces/PlayerHealth.ts +12 -1
  52. package/src/interfaces/RoomDescription.ts +1 -0
  53. package/dist/functions/itemPool.d.ts +0 -10
  54. package/dist/functions/itemPool.d.ts.map +0 -1
  55. package/dist/functions/itemPool.lua +0 -116
  56. package/src/functions/itemPool.ts +0 -153
@@ -2,6 +2,7 @@ import {
2
2
  ActiveSlot,
3
3
  CollectibleType,
4
4
  DamageFlag,
5
+ EntityFlag,
5
6
  EntityType,
6
7
  GridCollisionClass,
7
8
  GridEntityType,
@@ -15,7 +16,7 @@ import { ModUpgraded } from "../classes/ModUpgraded";
15
16
  import { DecorationVariant } from "../enums/DecorationVariant";
16
17
  import { ModCallbackCustom } from "../enums/ModCallbackCustom";
17
18
  import { errorIfFeaturesNotInitialized } from "../featuresInitialized";
18
- import { spawnNPC } from "../functions/entitiesSpecific";
19
+ import { spawn } from "../functions/entities";
19
20
  import { hasFlag } from "../functions/flag";
20
21
  import {
21
22
  removeGridEntity,
@@ -23,22 +24,23 @@ import {
23
24
  } from "../functions/gridEntities";
24
25
  import { getRoomListIndex } from "../functions/roomData";
25
26
  import { isVector } from "../functions/vector";
26
- import { CustomGridEntityData } from "../interfaces/CustomGridEntityData";
27
+ import { GridEntityCustomData } from "../interfaces/GridEntityCustomData";
27
28
  import { runNextGameFrame } from "./runInNFrames";
28
29
  import { saveDataManager } from "./saveDataManager/exports";
29
30
 
30
31
  const FEATURE_NAME = "customGridEntity";
32
+ const GENERIC_PROP_SIZE_MULTIPLIER = 0.66;
31
33
 
32
34
  const v = {
33
35
  level: {
34
36
  /** Indexed by room list index and grid index. */
35
- customGridEntities: new DefaultMap<int, Map<int, CustomGridEntityData>>(
37
+ customGridEntities: new DefaultMap<int, Map<int, GridEntityCustomData>>(
36
38
  () => new Map(),
37
39
  ),
38
40
  },
39
41
 
40
42
  room: {
41
- dummyPtrHashes: new Set<PtrHash>(),
43
+ genericPropPtrHashes: new Set<PtrHash>(),
42
44
  manuallyUsingShovel: false,
43
45
  },
44
46
  };
@@ -49,8 +51,8 @@ export function customGridEntityInit(mod: ModUpgraded): void {
49
51
 
50
52
  mod.AddCallback(
51
53
  ModCallback.ENTITY_TAKE_DMG,
52
- entityTakeDmgDummy,
53
- EntityType.DUMMY,
54
+ entityTakeDmgGenericProp,
55
+ EntityType.GENERIC_PROP,
54
56
  ); // 11
55
57
 
56
58
  mod.AddCallback(
@@ -66,8 +68,8 @@ export function customGridEntityInit(mod: ModUpgraded): void {
66
68
  }
67
69
 
68
70
  // ModCallback.ENTITY_TAKE_DMG (11)
69
- // EntityType.DUMMY (964)
70
- function entityTakeDmgDummy(
71
+ // EntityType.GENERIC_PROP (960)
72
+ function entityTakeDmgGenericProp(
71
73
  tookDamage: Entity,
72
74
  _damageAmount: float,
73
75
  damageFlags: BitFlags<DamageFlag>,
@@ -75,7 +77,7 @@ function entityTakeDmgDummy(
75
77
  _damageCountdownFrames: int,
76
78
  ): boolean | undefined {
77
79
  const ptrHash = GetPtrHash(tookDamage);
78
- if (!v.room.dummyPtrHashes.has(ptrHash)) {
80
+ if (!v.room.genericPropPtrHashes.has(ptrHash)) {
79
81
  return undefined;
80
82
  }
81
83
 
@@ -105,8 +107,8 @@ function entityTakeDmgDummy(
105
107
 
106
108
  postGridEntityCustomBrokenFire(gridEntity, data.gridEntityTypeCustom);
107
109
 
108
- // Even though the custom grid entity is now broke, we do not want to remove it, as the end-user
109
- // could intend for it to persist with different graphics.
110
+ // Even though the custom grid entity is now broken, we do not want to remove it, as the end-user
111
+ // could intend for it to persist with different graphics (or take multiple hits to be destroyed).
110
112
  return false;
111
113
  }
112
114
 
@@ -261,6 +263,7 @@ export function spawnCustomGridEntity(
261
263
  if (decoration === undefined) {
262
264
  error("Failed to spawn a decoration for a custom grid entity.");
263
265
  }
266
+ decoration.CollisionClass = gridCollisionClass;
264
267
 
265
268
  const sprite = decoration.GetSprite();
266
269
  sprite.Load(anm2Path, true);
@@ -270,7 +273,7 @@ export function spawnCustomGridEntity(
270
273
  : defaultAnimation;
271
274
  sprite.Play(animationToPlay, true);
272
275
 
273
- const customGridEntityData: CustomGridEntityData = {
276
+ const customGridEntityData: GridEntityCustomData = {
274
277
  gridEntityTypeCustom,
275
278
  roomListIndex,
276
279
  gridIndex,
@@ -287,8 +290,18 @@ export function spawnCustomGridEntity(
287
290
  // the monitoring for explosions in the `ENTITY_TAKE_DMG` callback.
288
291
  if (breakable) {
289
292
  const position = room.GetGridPosition(gridIndex);
290
- const dummy = spawnNPC(EntityType.DUMMY, 0, 0, position);
291
- dummy.Visible = false;
293
+ const entity = spawn(EntityType.GENERIC_PROP, 0, 0, position);
294
+ entity.ClearEntityFlags(EntityFlag.APPEAR);
295
+ entity.Visible = false;
296
+
297
+ // By default, it is larger than a grid tile, so make it a bit smaller.
298
+ entity.SizeMulti = Vector(
299
+ GENERIC_PROP_SIZE_MULTIPLIER,
300
+ GENERIC_PROP_SIZE_MULTIPLIER,
301
+ );
302
+
303
+ const ptrHash = GetPtrHash(entity);
304
+ v.room.genericPropPtrHashes.add(ptrHash);
292
305
  }
293
306
 
294
307
  return decoration;
@@ -309,7 +322,7 @@ export function spawnCustomGridEntity(
309
322
  * @returns The grid entity that was removed. Returns undefined if no grid entity was found at the
310
323
  * given location or if the given grid entity was not a custom grid entity.
311
324
  */
312
- export function removeCustomGrid(
325
+ export function removeCustomGridEntity(
313
326
  gridIndexOrPositionOrGridEntity: int | Vector | GridEntity,
314
327
  updateRoom = true,
315
328
  ): GridEntity | undefined {
@@ -356,7 +369,7 @@ export function removeCustomGrid(
356
369
  * containing the raw decoration grid entity and the associated entity data.
357
370
  */
358
371
  export function getCustomGridEntities(): Array<
359
- [gridEntity: GridEntity, data: CustomGridEntityData]
372
+ [gridEntity: GridEntity, data: GridEntityCustomData]
360
373
  > {
361
374
  const roomListIndex = getRoomListIndex();
362
375
  const roomCustomGridEntities = v.level.customGridEntities.get(roomListIndex);
@@ -365,7 +378,7 @@ export function getCustomGridEntities(): Array<
365
378
  }
366
379
 
367
380
  const room = game.GetRoom();
368
- const customGridEntities: Array<[GridEntity, CustomGridEntityData]> = [];
381
+ const customGridEntities: Array<[GridEntity, GridEntityCustomData]> = [];
369
382
  for (const [gridIndex, data] of roomCustomGridEntities.entries()) {
370
383
  const gridEntity = room.GetGridEntity(gridIndex);
371
384
  if (gridEntity !== undefined) {
@@ -64,7 +64,7 @@ import { getEnumValues } from "../../functions/enums";
64
64
  import { addFlag } from "../../functions/flag";
65
65
  import { spawnGridEntity } from "../../functions/gridEntities";
66
66
  import {
67
- logEffects,
67
+ logPlayerEffects,
68
68
  logRoom,
69
69
  logSeedEffects,
70
70
  logSounds,
@@ -515,7 +515,7 @@ export function dungeon(): void {
515
515
  /** Logs the player's current temporary effects to the "log.txt" file. */
516
516
  export function effects(): void {
517
517
  const player = Isaac.GetPlayer();
518
- logEffects(player);
518
+ logPlayerEffects(player);
519
519
  printConsole('Logged the player\'s effects to the "log.txt" file.');
520
520
  }
521
521
 
@@ -8,7 +8,7 @@ import { ModCallbackCustom } from "../enums/ModCallbackCustom";
8
8
  import { errorIfFeaturesNotInitialized } from "../featuresInitialized";
9
9
  import { spawn } from "../functions/entities";
10
10
  import { getRoomListIndex } from "../functions/roomData";
11
- import { getLatestRoomDescription } from "./roomHistory";
11
+ import { getLatestRoomDescription, isLeavingRoom } from "./roomHistory";
12
12
  import { saveDataManager } from "./saveDataManager/exports";
13
13
 
14
14
  interface PersistentEntityDescription {
@@ -56,6 +56,10 @@ export function persistentEntitiesInit(mod: ModUpgraded): void {
56
56
 
57
57
  // ModCallback.POST_ENTITY_REMOVE (67)
58
58
  function postEntityRemove(entity: Entity) {
59
+ checkDespawningFromPlayerLeavingRoom(entity);
60
+ }
61
+
62
+ function checkDespawningFromPlayerLeavingRoom(entity: Entity) {
59
63
  const ptrHash = GetPtrHash(entity);
60
64
  const tuple = v.room.spawnedPersistentEntities.get(ptrHash);
61
65
  if (tuple === undefined) {
@@ -63,10 +67,20 @@ function postEntityRemove(entity: Entity) {
63
67
  }
64
68
  const index = tuple[0];
65
69
 
66
- // The persistent entity is despawning, presumably because the player is in the process of leaving
67
- // the room. Keep track of the position for later. We use the previous room list index because
68
- // even though the `POST_NEW_ROOM` callback was not fired yet, we have already traveled to the
69
- // next room.
70
+ if (!isLeavingRoom()) {
71
+ return;
72
+ }
73
+
74
+ trackDespawningPickupPosition(entity, index);
75
+ }
76
+
77
+ /**
78
+ * The persistent entity is despawning because the player is in the process of leaving the room.
79
+ * Keep track of the position for later.
80
+ */
81
+ function trackDespawningPickupPosition(entity: Entity, index: int) {
82
+ // (The "latest" room description is really the previous room, because the `POST_NEW_ROOM`
83
+ // callback was not fired yet.)
70
84
  const previousRoomDescription = getLatestRoomDescription();
71
85
  const previousRoomListIndex = previousRoomDescription.roomListIndex;
72
86
  const persistentEntityDescription: PersistentEntityDescription = {
@@ -14,7 +14,7 @@ import { getRoomListIndex } from "../functions/roomData";
14
14
  import { onAscent } from "../functions/stage";
15
15
  import { vectorEquals } from "../functions/vector";
16
16
  import { PickupIndex } from "../types/PickupIndex";
17
- import { getLatestRoomDescription } from "./roomHistory";
17
+ import { getLatestRoomDescription, isLeavingRoom } from "./roomHistory";
18
18
  import { saveDataManager } from "./saveDataManager/exports";
19
19
 
20
20
  interface PickupDescription {
@@ -27,7 +27,6 @@ const FEATURE_NAME = "pickupIndex";
27
27
  const v = {
28
28
  run: {
29
29
  pickupCounter: 0 as PickupIndex,
30
- currentRoomListIndex: 0,
31
30
 
32
31
  pickupDataTreasureRooms: new Map<PickupIndex, PickupDescription>(),
33
32
  pickupDataBossRooms: new Map<PickupIndex, PickupDescription>(),
@@ -81,9 +80,6 @@ function postPickupInit(pickup: EntityPickup) {
81
80
 
82
81
  v.run.pickupCounter++;
83
82
  v.room.pickupIndexes.set(ptrHash, v.run.pickupCounter);
84
-
85
- // Additionally, keep track of which room we are storing the pointer hashes for.
86
- v.run.currentRoomListIndex = getRoomListIndex();
87
83
  }
88
84
 
89
85
  // ModCallback.POST_ENTITY_REMOVE (67)
@@ -99,10 +95,7 @@ function checkDespawningFromPlayerLeavingRoom(entity: Entity) {
99
95
  return;
100
96
  }
101
97
 
102
- const roomListIndex = getRoomListIndex();
103
- if (roomListIndex === v.run.currentRoomListIndex) {
104
- // This is a pickup that is despawning in the current room. For example, it could be a heart
105
- // pickup that the player picked up. Thus, we do not need to keep track of it's metadata.
98
+ if (!isLeavingRoom()) {
106
99
  return;
107
100
  }
108
101
 
@@ -111,13 +104,14 @@ function checkDespawningFromPlayerLeavingRoom(entity: Entity) {
111
104
 
112
105
  /**
113
106
  * This is a pickup that is despawning because the player is in the process of leaving the room.
114
- * Keep track of the metadata for later. We need to use the previous room list index because even
115
- * though the `POST_NEW_ROOM` callback was not fired yet, we have already traveled to the next room.
107
+ * Keep track of the metadata for later.
116
108
  */
117
109
  function trackDespawningPickupMetadata(
118
110
  entity: Entity,
119
111
  pickupIndex: PickupIndex,
120
112
  ) {
113
+ // The "latest" room description is really the previous room, because the `POST_NEW_ROOM` callback
114
+ // was not fired yet.
121
115
  const previousRoomDescription = getLatestRoomDescription();
122
116
  const previousRoomListIndex = previousRoomDescription.roomListIndex;
123
117
  const pickupDescriptions = v.level.pickupData.getAndSetDefault(
@@ -176,10 +170,10 @@ function postNewRoomReordered() {
176
170
  }
177
171
 
178
172
  if (pickupIndex === undefined) {
179
- const entityID = getEntityID(pickup);
180
- error(
181
- `Failed to find a pickup index corresponding to existing pickup: ${entityID}`,
182
- );
173
+ // At this point, if we do not already have an existing pickup index, we need to create a new
174
+ // one in order to cover the cases where mods spawn items in the `POST_NEW_ROOM` callback.
175
+ v.run.pickupCounter++;
176
+ pickupIndex = v.run.pickupCounter;
183
177
  }
184
178
 
185
179
  const ptrHash = GetPtrHash(pickup);
@@ -10,6 +10,7 @@ import {
10
10
  getRoomStageID,
11
11
  getRoomSubType,
12
12
  getRoomVariant,
13
+ getRoomVisitedCount,
13
14
  } from "../functions/roomData";
14
15
  import { RoomDescription } from "../interfaces/RoomDescription";
15
16
  import { saveDataManager } from "./saveDataManager/exports";
@@ -45,6 +46,7 @@ function postNewRoomEarly() {
45
46
  const roomName = getRoomName();
46
47
  const roomGridIndex = getRoomGridIndex();
47
48
  const roomListIndex = getRoomListIndex();
49
+ const roomVisitedCount = getRoomVisitedCount();
48
50
 
49
51
  const roomDescription: RoomDescription = {
50
52
  stage,
@@ -56,6 +58,7 @@ function postNewRoomEarly() {
56
58
  roomName,
57
59
  roomGridIndex,
58
60
  roomListIndex,
61
+ roomVisitedCount,
59
62
  };
60
63
  v.run.roomHistory.push(roomDescription);
61
64
  }
@@ -96,10 +99,7 @@ export function getPreviousRoomDescription(): RoomDescription {
96
99
  * Helper function to get information about the most recent room that is stored in the room history
97
100
  * array.
98
101
  *
99
- * This is useful in the `POST_ENTITY_REMOVE` callback, since if an entity is despawning due to a
100
- * player having left the room, the current room will have changed already, but the `POST_NEW_ROOM`
101
- * callback will not have fired yet, and there will not be an entry in the room history array for
102
- * the current room.
102
+ * This is useful in the `POST_ENTITY_REMOVE` callback; see the `isLeavingRoom` function.
103
103
  */
104
104
  export function getLatestRoomDescription(): RoomDescription {
105
105
  const latestRoomDescription = getLastElement(v.run.roomHistory);
@@ -111,3 +111,23 @@ export function getLatestRoomDescription(): RoomDescription {
111
111
 
112
112
  return latestRoomDescription;
113
113
  }
114
+
115
+ /**
116
+ * Helper function to detect if the game is in the state where the room index has changed to a new
117
+ * room, but the entities from the previous room are currently in the process of despawning. (At
118
+ * this point, the `POST_NEW_ROOM` callback will not have fired yet, and there will not be an entry
119
+ * in the room history array for the current room.)
120
+ *
121
+ * This function is intended to be used in the `POST_ENTITY_REMOVE` callback to detect when an
122
+ * entity is pseudo-persistent entity such as a pickup is despawning.
123
+ */
124
+ export function isLeavingRoom(): boolean {
125
+ const roomListIndex = getRoomListIndex();
126
+ const roomVisitedCount = getRoomVisitedCount();
127
+ const latestRoomDescription = getLatestRoomDescription();
128
+
129
+ return (
130
+ roomListIndex !== latestRoomDescription.roomListIndex ||
131
+ roomVisitedCount !== latestRoomDescription.roomVisitedCount
132
+ );
133
+ }
@@ -6,6 +6,7 @@ import {
6
6
  GameStateFlag,
7
7
  GridEntityType,
8
8
  GridRoom,
9
+ HeartSubType,
9
10
  LevelStateFlag,
10
11
  ProjectileFlag,
11
12
  SeedEffect,
@@ -202,34 +203,6 @@ export function logDamageFlags(
202
203
  logFlags(flags, DamageFlag, "damage");
203
204
  }
204
205
 
205
- export function logEffects(this: void, player: EntityPlayer): void {
206
- const effects = getEffectsList(player);
207
-
208
- log("Logging player effects:");
209
-
210
- if (effects.length === 0) {
211
- log(" n/a (no effects)");
212
- return;
213
- }
214
-
215
- effects.forEach((effect, i) => {
216
- let effectDescription: string;
217
- if (effect.Item.IsCollectible()) {
218
- const collectibleName = getCollectibleName(effect.Item.ID);
219
- effectDescription = `Collectible: ${collectibleName}`;
220
- } else if (effect.Item.IsTrinket()) {
221
- const trinketName = getTrinketName(effect.Item.ID);
222
- effectDescription = `Trinket: ${trinketName}`;
223
- } else if (effect.Item.IsNull()) {
224
- effectDescription = `Null item: ${effect.Item.ID}`;
225
- } else {
226
- effectDescription = `Unknown type of effect: ${effect.Item.ID}`;
227
- }
228
-
229
- log(` ${i + 1}) ${effectDescription} (x${effect.Count})`);
230
- });
231
- }
232
-
233
206
  /** Helper function for logging an array of specific entities. */
234
207
  export function logEntities(this: void, entities: Entity[]): void {
235
208
  for (const entity of entities) {
@@ -504,6 +477,34 @@ export function logMap(this: void, map: Map<AnyNotNil, unknown>): void {
504
477
  log(` The size of the map was: ${map.size}`);
505
478
  }
506
479
 
480
+ export function logPlayerEffects(this: void, player: EntityPlayer): void {
481
+ const effects = getEffectsList(player);
482
+
483
+ log("Logging player effects:");
484
+
485
+ if (effects.length === 0) {
486
+ log(" n/a (no effects)");
487
+ return;
488
+ }
489
+
490
+ effects.forEach((effect, i) => {
491
+ let effectDescription: string;
492
+ if (effect.Item.IsCollectible()) {
493
+ const collectibleName = getCollectibleName(effect.Item.ID);
494
+ effectDescription = `Collectible: ${collectibleName}`;
495
+ } else if (effect.Item.IsTrinket()) {
496
+ const trinketName = getTrinketName(effect.Item.ID);
497
+ effectDescription = `Trinket: ${trinketName}`;
498
+ } else if (effect.Item.IsNull()) {
499
+ effectDescription = `Null item: ${effect.Item.ID}`;
500
+ } else {
501
+ effectDescription = `Unknown type of effect: ${effect.Item.ID}`;
502
+ }
503
+
504
+ log(` ${i + 1}) ${effectDescription} (x${effect.Count})`);
505
+ });
506
+ }
507
+
507
508
  export function logPlayerHealth(this: void, player: EntityPlayer): void {
508
509
  const playerName = getPlayerName(player);
509
510
  const playerHealth = getPlayerHealth(player);
@@ -519,7 +520,11 @@ export function logPlayerHealth(this: void, player: EntityPlayer): void {
519
520
  log(` Broken hearts: ${playerHealth.brokenHearts}`);
520
521
  log(` Soul charges: ${playerHealth.soulCharges}`);
521
522
  log(` Blood charges: ${playerHealth.bloodCharges}`);
522
- log(` Soul heart types: [${playerHealth.soulHeartTypes.join(",")}]`);
523
+ log(" Soul heart types: [");
524
+ for (const soulHeartType of playerHealth.soulHeartTypes) {
525
+ log(` HeartSubType.${HeartSubType[soulHeartType]}`);
526
+ }
527
+ log(" ]");
523
528
  }
524
529
 
525
530
  /**
@@ -764,7 +769,6 @@ export function setLogFunctionsGlobal(): void {
764
769
  globals["logArray"] = logArray;
765
770
  globals["logColor"] = logColor;
766
771
  globals["logDamageFlags"] = logDamageFlags;
767
- globals["logEffects"] = logEffects;
768
772
  globals["logEntities"] = logEntities;
769
773
  globals["logEntity"] = logEntity;
770
774
  globals["logEntityID"] = logEntityID;
@@ -777,6 +781,7 @@ export function setLogFunctionsGlobal(): void {
777
781
  globals["logKColor"] = logKColor;
778
782
  globals["logLevelStateFlags"] = logLevelStateFlags;
779
783
  globals["logMap"] = logMap;
784
+ globals["logPlayerEffects"] = logPlayerEffects;
780
785
  globals["logPlayerHealth"] = logPlayerHealth;
781
786
  globals["logProjectileFlags"] = logProjectileFlags;
782
787
  globals["logRoom"] = logRoom;
@@ -6,6 +6,7 @@ import {
6
6
  DarkEsauVariant,
7
7
  DeathVariant,
8
8
  EntityType,
9
+ HopperVariant,
9
10
  MamaGurdyVariant,
10
11
  MotherSubType,
11
12
  MotherVariant,
@@ -83,17 +84,39 @@ export function isAliveExceptionNPC(npc: EntityNPC): boolean {
83
84
  return true;
84
85
  }
85
86
 
86
- if (isRaglingDeathPatch(npc)) {
87
+ // EntityType.HOPPER (29)
88
+ // HopperVariant.EGGY (2)
89
+ if (isDyingEggyWithNoSpidersLeft(npc)) {
87
90
  return true;
88
91
  }
89
92
 
90
- if (isDyingEggyWithNoSpidersLeft(npc)) {
93
+ // EntityType.DADDY_LONG_LEGS (101)
94
+ if (isDaddyLongLegsChildStompEntity(npc)) {
95
+ return true;
96
+ }
97
+
98
+ // EntityType.RAGLING (256)
99
+ if (isRaglingDeathPatch(npc)) {
91
100
  return true;
92
101
  }
93
102
 
94
103
  return false;
95
104
  }
96
105
 
106
+ /**
107
+ * Helper function to distinguish between a normal Daddy Long Legs / Triachnid and the child entity
108
+ * that is spawned when the boss does the multi-stomp attack.
109
+ *
110
+ * When this attack occurs, four extra copies of Daddy Long Legs will be spawned with the same
111
+ * entity type, variant, and sub-type. The `Entity.Parent` property will be undefined in this case,
112
+ * so the way to tell them apart is to check for a non-undefined `Entity.SpawnerEntity` property.
113
+ */
114
+ export function isDaddyLongLegsChildStompEntity(npc: EntityNPC): boolean {
115
+ return (
116
+ npc.Type === EntityType.DADDY_LONG_LEGS && npc.SpawnerEntity !== undefined
117
+ );
118
+ }
119
+
97
120
  /**
98
121
  * Helper function to detect the custom death state of an Eggy. Eggies are never actually marked
99
122
  * dead by the game. Instead, when Eggies take fatal damage, they go into NpcState.STATE_SUICIDE and
@@ -101,6 +124,8 @@ export function isAliveExceptionNPC(npc: EntityNPC): boolean {
101
124
  */
102
125
  export function isDyingEggyWithNoSpidersLeft(npc: EntityNPC): boolean {
103
126
  return (
127
+ npc.Type === EntityType.HOPPER &&
128
+ npc.Variant === (HopperVariant.EGGY as int) &&
104
129
  npc.State === NpcState.SUICIDE &&
105
130
  npc.StateFrame >= EGGY_STATE_FRAME_OF_FINAL_SPIDER
106
131
  );
@@ -6,7 +6,7 @@ import {
6
6
  } from "isaac-typescript-definitions";
7
7
  import { MAX_PLAYER_HEART_CONTAINERS } from "../constants";
8
8
  import { HealthType } from "../enums/HealthType";
9
- import { PlayerHealth } from "../interfaces/PlayerHealth";
9
+ import { PlayerHealth, SoulHeartType } from "../interfaces/PlayerHealth";
10
10
  import { getTotalCharge } from "./charge";
11
11
  import { getEnumValues } from "./enums";
12
12
  import {
@@ -79,7 +79,6 @@ export function addPlayerHealthType(
79
79
  */
80
80
  export function getPlayerHealth(player: EntityPlayer): PlayerHealth {
81
81
  const character = player.GetPlayerType();
82
- const soulHeartTypes: HeartSubType[] = [];
83
82
  let maxHearts = player.GetMaxHearts();
84
83
  let hearts = getPlayerHearts(player); // We use the helper function to remove rotten hearts
85
84
  let soulHearts = player.GetSoulHearts();
@@ -115,6 +114,7 @@ export function getPlayerHealth(player: EntityPlayer): PlayerHealth {
115
114
  // track which soul heart we're currently at.
116
115
  let currentSoulHeart = 0;
117
116
 
117
+ const soulHeartTypes: SoulHeartType[] = [];
118
118
  for (let i = 0; i < extraHearts; i++) {
119
119
  let isBoneHeart = player.IsBoneHeart(i);
120
120
  if (character === PlayerType.THE_FORGOTTEN && subPlayer !== undefined) {
@@ -333,24 +333,37 @@ export function setPlayerHealth(
333
333
 
334
334
  // Add the soul / black / bone hearts.
335
335
  let soulHeartsRemaining = playerHealth.soulHearts;
336
- playerHealth.soulHeartTypes.forEach((heartType, i) => {
336
+ playerHealth.soulHeartTypes.forEach((soulHeartType, i) => {
337
337
  const isHalf =
338
338
  playerHealth.soulHearts + playerHealth.boneHearts * 2 < (i + 1) * 2;
339
339
  let addAmount = 2;
340
- if (isHalf || heartType === HeartSubType.BONE || soulHeartsRemaining < 2) {
340
+ if (
341
+ isHalf ||
342
+ soulHeartType === HeartSubType.BONE ||
343
+ soulHeartsRemaining < 2
344
+ ) {
341
345
  // Fix the bug where a half soul heart to the left of a bone heart will be treated as a full
342
346
  // soul heart.
343
347
  addAmount = 1;
344
348
  }
345
349
 
346
- if (heartType === HeartSubType.SOUL) {
347
- player.AddSoulHearts(addAmount);
348
- soulHeartsRemaining -= addAmount;
349
- } else if (heartType === HeartSubType.BLACK) {
350
- player.AddBlackHearts(addAmount);
351
- soulHeartsRemaining -= addAmount;
352
- } else if (heartType === HeartSubType.BONE) {
353
- player.AddBoneHearts(addAmount);
350
+ switch (soulHeartType) {
351
+ case HeartSubType.SOUL: {
352
+ player.AddSoulHearts(addAmount);
353
+ soulHeartsRemaining -= addAmount;
354
+ break;
355
+ }
356
+
357
+ case HeartSubType.BLACK: {
358
+ player.AddBlackHearts(addAmount);
359
+ soulHeartsRemaining -= addAmount;
360
+ break;
361
+ }
362
+
363
+ case HeartSubType.BONE: {
364
+ player.AddBoneHearts(addAmount);
365
+ break;
366
+ }
354
367
  }
355
368
  });
356
369