isaacscript-common 6.20.2 → 6.21.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 (91) hide show
  1. package/dist/constantsFirstLast.d.ts +2 -2
  2. package/dist/constantsFirstLast.lua +2 -2
  3. package/dist/enums/RockAltType.d.ts +12 -1
  4. package/dist/enums/RockAltType.d.ts.map +1 -1
  5. package/dist/enums/RockAltType.lua +4 -2
  6. package/dist/features/customStage/backdrop.d.ts.map +1 -1
  7. package/dist/features/customStage/backdrop.lua +3 -2
  8. package/dist/features/customStage/customStageConstants.d.ts +11 -0
  9. package/dist/features/customStage/customStageConstants.d.ts.map +1 -1
  10. package/dist/features/customStage/customStageConstants.lua +10 -0
  11. package/dist/features/customStage/customStageGridEntities.d.ts +1 -0
  12. package/dist/features/customStage/customStageGridEntities.d.ts.map +1 -1
  13. package/dist/features/customStage/customStageGridEntities.lua +58 -23
  14. package/dist/features/customStage/exports.d.ts +4 -5
  15. package/dist/features/customStage/exports.d.ts.map +1 -1
  16. package/dist/features/customStage/exports.lua +49 -51
  17. package/dist/features/customStage/init.d.ts.map +1 -1
  18. package/dist/features/customStage/init.lua +7 -12
  19. package/dist/features/customStage/shadows.d.ts.map +1 -1
  20. package/dist/features/customStage/shadows.lua +2 -1
  21. package/dist/features/customStage/streakText.d.ts +0 -7
  22. package/dist/features/customStage/streakText.d.ts.map +1 -1
  23. package/dist/features/customStage/streakText.lua +51 -84
  24. package/dist/features/customStage/v.d.ts +13 -0
  25. package/dist/features/customStage/v.d.ts.map +1 -1
  26. package/dist/features/customStage/v.lua +6 -1
  27. package/dist/features/customTrapdoor/exports.d.ts +7 -6
  28. package/dist/features/customTrapdoor/exports.d.ts.map +1 -1
  29. package/dist/features/customTrapdoor/exports.lua +6 -5
  30. package/dist/features/customTrapdoor/init.d.ts.map +1 -1
  31. package/dist/features/customTrapdoor/init.lua +11 -6
  32. package/dist/features/customTrapdoor/touched.lua +0 -1
  33. package/dist/features/customTrapdoor/v.d.ts +1 -1
  34. package/dist/features/extraConsoleCommands/commandsSubroutines.d.ts.map +1 -1
  35. package/dist/features/extraConsoleCommands/commandsSubroutines.lua +3 -3
  36. package/dist/features/saveDataManager/exports.d.ts +3 -0
  37. package/dist/features/saveDataManager/exports.d.ts.map +1 -1
  38. package/dist/features/saveDataManager/exports.lua +3 -0
  39. package/dist/functions/collectibleSet.d.ts +1 -1
  40. package/dist/functions/collectibleSet.lua +1 -1
  41. package/dist/functions/doors.d.ts +10 -0
  42. package/dist/functions/doors.d.ts.map +1 -1
  43. package/dist/functions/doors.lua +6 -0
  44. package/dist/functions/log.d.ts +1 -15
  45. package/dist/functions/log.d.ts.map +1 -1
  46. package/dist/functions/log.lua +3 -218
  47. package/dist/functions/logEntities.d.ts +16 -0
  48. package/dist/functions/logEntities.d.ts.map +1 -0
  49. package/dist/functions/logEntities.lua +220 -0
  50. package/dist/functions/rockAlt.d.ts +6 -5
  51. package/dist/functions/rockAlt.d.ts.map +1 -1
  52. package/dist/functions/rockAlt.lua +147 -18
  53. package/dist/functions/roomTransition.d.ts +26 -0
  54. package/dist/functions/roomTransition.d.ts.map +1 -0
  55. package/dist/functions/roomTransition.lua +75 -0
  56. package/dist/functions/rooms.d.ts +1 -18
  57. package/dist/functions/rooms.d.ts.map +1 -1
  58. package/dist/functions/rooms.lua +3 -52
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.lua +8 -0
  62. package/dist/interfaces/private/CustomTrapdoorDescription.d.ts +1 -1
  63. package/dist/interfaces/private/CustomTrapdoorDescription.d.ts.map +1 -1
  64. package/dist/objects/backdropTypeToRockAltType.lua +3 -3
  65. package/package.json +1 -1
  66. package/src/constantsFirstLast.ts +2 -2
  67. package/src/enums/RockAltType.ts +14 -1
  68. package/src/features/customStage/backdrop.ts +2 -1
  69. package/src/features/customStage/customStageConstants.ts +16 -0
  70. package/src/features/customStage/customStageGridEntities.ts +47 -0
  71. package/src/features/customStage/exports.ts +44 -40
  72. package/src/features/customStage/init.ts +7 -18
  73. package/src/features/customStage/shadows.ts +2 -1
  74. package/src/features/customStage/streakText.ts +58 -95
  75. package/src/features/customStage/v.ts +17 -0
  76. package/src/features/customTrapdoor/exports.ts +9 -6
  77. package/src/features/customTrapdoor/init.ts +16 -5
  78. package/src/features/customTrapdoor/touched.ts +0 -2
  79. package/src/features/customTrapdoor/v.ts +1 -1
  80. package/src/features/extraConsoleCommands/commandsSubroutines.ts +4 -1
  81. package/src/features/saveDataManager/exports.ts +3 -0
  82. package/src/functions/collectibleSet.ts +1 -1
  83. package/src/functions/doors.ts +10 -0
  84. package/src/functions/log.ts +1 -279
  85. package/src/functions/logEntities.ts +276 -0
  86. package/src/functions/rockAlt.ts +147 -19
  87. package/src/functions/roomTransition.ts +78 -0
  88. package/src/functions/rooms.ts +2 -60
  89. package/src/index.ts +1 -0
  90. package/src/interfaces/private/CustomTrapdoorDescription.ts +3 -1
  91. package/src/objects/backdropTypeToRockAltType.ts +3 -3
@@ -1,11 +1,13 @@
1
1
  import {
2
2
  CollectibleType,
3
3
  EntityType,
4
+ GridEntityType,
4
5
  TrinketType,
5
6
  } from "isaac-typescript-definitions";
6
7
  import { DecorationVariant } from "../../enums/DecorationVariant";
7
8
  import { removeEntities } from "../../functions/entities";
8
9
  import { getNPCs } from "../../functions/entitiesSpecific";
10
+ import { removeGridEntity } from "../../functions/gridEntities";
9
11
  import {
10
12
  getCoins,
11
13
  getCollectibles,
@@ -13,6 +15,8 @@ import {
13
15
  } from "../../functions/pickupsSpecific";
14
16
  import { vectorEquals } from "../../functions/vector";
15
17
  import { CustomStage } from "../../interfaces/CustomStage";
18
+ import { spawnCustomTrapdoor } from "../customTrapdoor/exports";
19
+ import v from "./v";
16
20
 
17
21
  /** For `GridEntityType.DECORATION` (1) */
18
22
  export function setCustomDecorationGraphics(
@@ -25,6 +29,11 @@ export function setCustomDecorationGraphics(
25
29
  return;
26
30
  }
27
31
 
32
+ const gridEntityType = gridEntity.GetType();
33
+ if (gridEntityType !== GridEntityType.DECORATION) {
34
+ return;
35
+ }
36
+
28
37
  // Ignore custom grid entities. (They are represented as decorations with a non-zero variant.)
29
38
  const variant = gridEntity.GetVariant();
30
39
  if (variant !== (DecorationVariant.VANILLA_DECORATION as int)) {
@@ -51,6 +60,11 @@ export function setCustomRockGraphics(
51
60
  return;
52
61
  }
53
62
 
63
+ const gridEntityRock = gridEntity.ToRock();
64
+ if (gridEntityRock === undefined) {
65
+ return;
66
+ }
67
+
54
68
  const sprite = gridEntity.GetSprite();
55
69
  const fileName = sprite.GetFilename();
56
70
  if (fileName === "gfx/grid/grid_rock.anm2") {
@@ -73,6 +87,11 @@ export function setCustomPitGraphics(
73
87
  return;
74
88
  }
75
89
 
90
+ const gridEntityPit = gridEntity.ToPit();
91
+ if (gridEntityPit === undefined) {
92
+ return;
93
+ }
94
+
76
95
  const sprite = gridEntity.GetSprite();
77
96
  const fileName = sprite.GetFilename();
78
97
  if (fileName === "gfx/grid/grid_pit.anm2") {
@@ -86,6 +105,17 @@ export function setCustomDoorGraphics(
86
105
  customStage: CustomStage,
87
106
  gridEntity: GridEntity,
88
107
  ): void {
108
+ // If the end-user did not specify custom pit graphics, default to Basement graphics. (We don't
109
+ // have to adjust anything for this case.)
110
+ if (customStage.doorPNGPaths === undefined) {
111
+ return;
112
+ }
113
+
114
+ const gridEntityDoor = gridEntity.ToDoor();
115
+ if (gridEntityDoor === undefined) {
116
+ return;
117
+ }
118
+
89
119
  const sprite = gridEntity.GetSprite();
90
120
  const fileName = sprite.GetFilename();
91
121
  const doorPNGPath = getNewDoorPNGPath(customStage, fileName);
@@ -148,6 +178,23 @@ function getNewDoorPNGPath(
148
178
  return undefined;
149
179
  }
150
180
 
181
+ export function convertVanillaTrapdoors(
182
+ customStage: CustomStage,
183
+ gridEntity: GridEntity,
184
+ ): void {
185
+ const gridEntityType = gridEntity.GetType();
186
+ if (gridEntityType !== GridEntityType.TRAPDOOR) {
187
+ return;
188
+ }
189
+
190
+ removeGridEntity(gridEntity, true);
191
+
192
+ const destination: [string, int] | undefined = v.run.firstFloor
193
+ ? [customStage.name, 2]
194
+ : undefined;
195
+ spawnCustomTrapdoor(gridEntity.Position, destination);
196
+ }
197
+
151
198
  /**
152
199
  * The rewards are based on the ones from the wiki:
153
200
  * https://bindingofisaacrebirth.fandom.com/wiki/Rocks#Urns
@@ -8,15 +8,15 @@ import {
8
8
  import { game } from "../../cachedClasses";
9
9
  import { reorderedCallbacksSetStage } from "../../callbacks/reorderedCallbacks";
10
10
  import { getEntityIDFromConstituents } from "../../functions/entities";
11
- import { log, logError } from "../../functions/log";
11
+ import { logError } from "../../functions/log";
12
12
  import { newRNG } from "../../functions/rng";
13
13
  import {
14
14
  getRoomDataForTypeVariant,
15
15
  getRoomsInGrid,
16
16
  } from "../../functions/rooms";
17
17
  import { setStage } from "../../functions/stage";
18
+ import { CustomStage } from "../../interfaces/CustomStage";
18
19
  import { getRandomCustomStageRoom } from "./customStageUtils";
19
- import { topStreakTextStart } from "./streakText";
20
20
  import v, {
21
21
  customBossPNGPaths,
22
22
  customStageCachedRoomData,
@@ -33,13 +33,13 @@ const DEFAULT_BASE_STAGE_TYPE = StageType.ORIGINAL;
33
33
  * more details: https://isaacscript.github.io/main/custom-stages/
34
34
  *
35
35
  * @param name The name of the custom stage, corresponding to what is in the "tsconfig.json" file.
36
- * @param _firstFloor Whether to go to the first floor or the second floor. For example, if you have
37
- * a custom stage emulating Caves, then the first floor would be Caves 1, and the
38
- * second floor would be Caves 2.
36
+ * @param firstFloor Whether to go to the first floor or the second floor. For example, if you have
37
+ * a custom stage emulating Caves, then the first floor would be Caves 1, and the
38
+ * second floor would be Caves 2.
39
39
  */
40
40
  export function setCustomStage(
41
41
  name: string,
42
- _firstFloor = true,
42
+ firstFloor = true,
43
43
  verbose = false,
44
44
  ): void {
45
45
  const customStage = customStagesMap.get(name);
@@ -50,24 +50,53 @@ export function setCustomStage(
50
50
  }
51
51
 
52
52
  const level = game.GetLevel();
53
- const startingRoomGridIndex = level.GetStartingRoomIndex();
54
53
  const seeds = game.GetSeeds();
55
54
  const startSeed = seeds.GetStartSeed();
56
55
  const rng = newRNG(startSeed);
57
56
 
58
57
  v.run.currentCustomStage = customStage;
58
+ v.run.firstFloor = firstFloor;
59
59
 
60
- const baseStage =
60
+ let baseStage: int =
61
61
  customStage.baseStage === undefined
62
62
  ? DEFAULT_BASE_STAGE
63
- : (customStage.baseStage as LevelStage);
63
+ : customStage.baseStage;
64
+ if (!firstFloor) {
65
+ baseStage++;
66
+ }
64
67
  const baseStageType =
65
68
  customStage.baseStageType === undefined
66
69
  ? DEFAULT_BASE_STAGE_TYPE
67
70
  : (customStage.baseStageType as StageType);
68
- setStage(baseStage, baseStageType);
71
+ setStage(baseStage as LevelStage, baseStageType);
72
+
73
+ setStageRoomsData(customStage, rng, verbose);
74
+
75
+ // Set the stage to an invalid value, which will prevent the walls and floors from loading. We
76
+ // must use `StageType.WRATH_OF_THE_LAMB` instead of `StageType.ORIGINAL` or else the walls will
77
+ // not render properly. DeadInfinity suspects that this might be because it is trying to use the
78
+ // Dark Room's backdrop (instead of The Chest).
79
+ const stage = -1 as LevelStage;
80
+ const stageType = StageType.WRATH_OF_THE_LAMB;
81
+ level.SetStage(stage, stageType);
82
+ reorderedCallbacksSetStage(stage, stageType);
83
+
84
+ // We must reload the current room in order for the `Level.SetStage` method to take effect.
85
+ // Furthermore, we need to cancel the queued warp to the `GridRoom.DEBUG` room. We can accomplish
86
+ // both of these things by initiating a room transition to an arbitrary room. However, we rely on
87
+ // the parent function to do this, since for normal purposes, we need to initiate a room
88
+ // transition for the pixelation effect.
89
+ }
90
+
91
+ /** Pick a custom room for each vanilla room. */
92
+ function setStageRoomsData(
93
+ customStage: CustomStage,
94
+ rng: RNG,
95
+ verbose: boolean,
96
+ ) {
97
+ const level = game.GetLevel();
98
+ const startingRoomGridIndex = level.GetStartingRoomIndex();
69
99
 
70
- // Now, we need to pick a custom room for each vanilla room.
71
100
  for (const room of getRoomsInGrid()) {
72
101
  // The starting floor of each room should stay empty.
73
102
  if (room.SafeGridIndex === startingRoomGridIndex) {
@@ -85,7 +114,7 @@ export function setCustomStage(
85
114
  // special rooms.)
86
115
  if (roomType === RoomType.DEFAULT) {
87
116
  logError(
88
- `Failed to find any custom rooms for RoomType.${RoomType[roomType]} (${roomType}) for custom stage: ${name}`,
117
+ `Failed to find any custom rooms for RoomType.${RoomType[roomType]} (${roomType}) for custom stage: ${customStage.name}`,
89
118
  );
90
119
  }
91
120
  continue;
@@ -95,7 +124,7 @@ export function setCustomStage(
95
124
  const roomDoorSlotFlagMap = roomShapeMap.get(roomShape);
96
125
  if (roomDoorSlotFlagMap === undefined) {
97
126
  logError(
98
- `Failed to find any custom rooms for RoomType.${RoomType[roomType]} (${roomType}) + RoomShape.${RoomShape[roomShape]} (${roomShape}) for custom stage: ${name}`,
127
+ `Failed to find any custom rooms for RoomType.${RoomType[roomType]} (${roomType}) + RoomShape.${RoomShape[roomShape]} (${roomShape}) for custom stage: ${customStage.name}`,
99
128
  );
100
129
  continue;
101
130
  }
@@ -104,7 +133,7 @@ export function setCustomStage(
104
133
  const roomsMetadata = roomDoorSlotFlagMap.get(doorSlotFlags);
105
134
  if (roomsMetadata === undefined) {
106
135
  logError(
107
- `Failed to find any custom rooms for RoomType.${RoomType[roomType]} (${roomType}) + RoomShape.${RoomShape[roomShape]} (${roomShape}) + DoorSlotFlags ${doorSlotFlags} for custom stage: ${name}`,
136
+ `Failed to find any custom rooms for RoomType.${RoomType[roomType]} (${roomType}) + RoomShape.${RoomShape[roomShape]} (${roomShape}) + DoorSlotFlags ${doorSlotFlags} for custom stage: ${customStage.name}`,
108
137
  );
109
138
  continue;
110
139
  }
@@ -121,7 +150,7 @@ export function setCustomStage(
121
150
  );
122
151
  if (newRoomData === undefined) {
123
152
  logError(
124
- `Failed to get the room data for room variant ${randomRoom.variant} for custom stage: ${name}`,
153
+ `Failed to get the room data for room variant ${randomRoom.variant} for custom stage: ${customStage.name}`,
125
154
  );
126
155
  continue;
127
156
  }
@@ -131,31 +160,6 @@ export function setCustomStage(
131
160
 
132
161
  room.Data = newRoomData;
133
162
  }
134
-
135
- // Set the stage to an invalid value, which will prevent the walls and floors from loading. We
136
- // must use `StageType.WRATH_OF_THE_LAMB` instead of `StageType.ORIGINAL` or else the walls will
137
- // not render properly. DeadInfinity suspects that this might be because it is trying to use the
138
- // Dark Room's backdrop (instead of The Chest).
139
- const stage = -1 as LevelStage;
140
- const stageType = StageType.WRATH_OF_THE_LAMB;
141
- level.SetStage(stage, stageType);
142
- reorderedCallbacksSetStage(stage, stageType);
143
-
144
- // We must reload the current room in order for the `Level.SetStage` method to take effect.
145
- // Furthermore, we need to cancel the queued warp to the `GridRoom.DEBUG` room. We can accomplish
146
- // both of these things by initiating a room transition to an arbitrary room. However, we rely on
147
- // the parent function to do this, since for normal purposes, we need to initiate a room
148
- // transition for the pixelation effect.
149
- }
150
-
151
- export function setCustomStageDebug(): void {
152
- const customStage = v.run.currentCustomStage;
153
- if (customStage === null) {
154
- log("No custom stage is currently loaded.");
155
- return;
156
- }
157
-
158
- topStreakTextStart();
159
163
  }
160
164
 
161
165
  /**
@@ -18,6 +18,7 @@ import {
18
18
  import { saveDataManager } from "../saveDataManager/exports";
19
19
  import { setBackdrop } from "./backdrop";
20
20
  import {
21
+ convertVanillaTrapdoors,
21
22
  removeUrnRewards,
22
23
  setCustomDecorationGraphics,
23
24
  setCustomDoorGraphics,
@@ -26,12 +27,7 @@ import {
26
27
  } from "./customStageGridEntities";
27
28
  import * as metadataJSON from "./metadata.json"; // This will correspond to "metadata.lua" at run-time.
28
29
  import { setShadows } from "./shadows";
29
- import {
30
- streakTextGetShaderParams,
31
- streakTextInit,
32
- streakTextPostGameStarted,
33
- streakTextPostRender,
34
- } from "./streakText";
30
+ import { streakTextGetShaderParams, streakTextPostRender } from "./streakText";
35
31
  import v, { customStagesMap } from "./v";
36
32
  import {
37
33
  playVersusScreenAnimation,
@@ -47,12 +43,10 @@ export function customStageInit(mod: ModUpgraded): void {
47
43
  }
48
44
 
49
45
  saveDataManager("customStage", v);
50
- streakTextInit();
51
46
  versusScreenInit();
52
47
 
53
48
  mod.AddCallback(ModCallback.POST_RENDER, postRender); // 2
54
49
  mod.AddCallback(ModCallback.POST_CURSE_EVAL, postCurseEval); // 12
55
- mod.AddCallback(ModCallback.POST_GAME_STARTED, postGameStarted); // 15
56
50
  mod.AddCallback(ModCallback.GET_SHADER_PARAMS, getShaderParams); // 21
57
51
  mod.AddCallbackCustom(
58
52
  ModCallbackCustom.POST_GRID_ENTITY_BROKEN,
@@ -61,7 +55,7 @@ export function customStageInit(mod: ModUpgraded): void {
61
55
  );
62
56
  mod.AddCallbackCustom(
63
57
  ModCallbackCustom.POST_GRID_ENTITY_INIT,
64
- postGridEntityBrokenInit,
58
+ postGridEntityInit,
65
59
  );
66
60
  mod.AddCallbackCustom(
67
61
  ModCallbackCustom.POST_NEW_ROOM_REORDERED,
@@ -150,19 +144,13 @@ function postCurseEval(
150
144
  }
151
145
 
152
146
  // Prevent XL floors on custom stages, since the streak text will not work properly.
153
- if (hasFlag(curses, LevelCurse.MAZE)) {
154
- return removeFlag(curses, LevelCurse.MAZE);
147
+ if (hasFlag(curses, LevelCurse.LABYRINTH)) {
148
+ return removeFlag(curses, LevelCurse.LABYRINTH);
155
149
  }
156
150
 
157
151
  return undefined;
158
152
  }
159
153
 
160
- // ModCallback.POST_GAME_STARTED (15)
161
- function postGameStarted() {
162
- // We don't early return here because we need to unconditionally reset the sprites.
163
- streakTextPostGameStarted();
164
- }
165
-
166
154
  // ModCallback.GET_SHADER_PARAMS (22)
167
155
  function getShaderParams(
168
156
  shaderName: string,
@@ -188,7 +176,7 @@ function postGridEntityBrokenRockAlt(gridEntity: GridEntity) {
188
176
  }
189
177
 
190
178
  // ModCallbackCustom.POST_GRID_ENTITY_INIT
191
- function postGridEntityBrokenInit(gridEntity: GridEntity) {
179
+ function postGridEntityInit(gridEntity: GridEntity) {
192
180
  const customStage = v.run.currentCustomStage;
193
181
  if (customStage === null) {
194
182
  return;
@@ -198,6 +186,7 @@ function postGridEntityBrokenInit(gridEntity: GridEntity) {
198
186
  setCustomRockGraphics(customStage, gridEntity);
199
187
  setCustomPitGraphics(customStage, gridEntity);
200
188
  setCustomDoorGraphics(customStage, gridEntity);
189
+ convertVanillaTrapdoors(customStage, gridEntity);
201
190
  }
202
191
 
203
192
  // ModCallbackCustom.POST_NEW_ROOM_REORDERED
@@ -17,6 +17,7 @@ type ShadowAnimation = "1x1" | "1x2" | "2x1" | "2x2";
17
17
  * time passes, like most other effects.
18
18
  */
19
19
  const SHADOW_EFFECT_VARIANT = EffectVariant.LADDER;
20
+ const SHADOW_EFFECT_SUBTYPE = 102;
20
21
 
21
22
  /** The animation comes from StageAPI. */
22
23
  const ROOM_SHAPE_TO_SHADOW_ANIMATION: {
@@ -57,7 +58,7 @@ export function setShadows(customStage: CustomStage): void {
57
58
  // rendering throughout this animation.)
58
59
  const shadowEffect = spawnEffectWithSeed(
59
60
  SHADOW_EFFECT_VARIANT,
60
- 0,
61
+ SHADOW_EFFECT_SUBTYPE,
61
62
  centerPos,
62
63
  1 as Seed,
63
64
  );
@@ -7,15 +7,12 @@ import {
7
7
  getScreenTopCenterPos,
8
8
  } from "../../functions/ui";
9
9
  import { CustomStage } from "../../interfaces/CustomStage";
10
+ import {
11
+ UIStreakAnimation,
12
+ UI_STREAK_ANIMATION_END_FRAMES,
13
+ } from "./customStageConstants";
10
14
  import v from "./v";
11
15
 
12
- enum UIStreakAnimation {
13
- TEXT = "Text",
14
- TEXT_IN = "TextIn",
15
- TEXT_OUT = "TextOut",
16
- TEXT_STAY = "TextStay",
17
- }
18
-
19
16
  /** This must match the name of the shader in "shaders.xml". */
20
17
  const EMPTY_SHADER_NAME = "IsaacScript-RenderAboveHUD";
21
18
 
@@ -47,9 +44,6 @@ const STREAK_TEXT_BOTTOM_Y_OFFSET = -9;
47
44
  */
48
45
  const NUM_RENDER_FRAMES_MAP_HELD_BEFORE_STREAK_TEXT = 11;
49
46
 
50
- /** Corresponds to the vanilla value; determined through trial and error. */
51
- const TEXT_PLAYBACK_SPEED = 0.5;
52
-
53
47
  /** Taken from StageAPI. */
54
48
  const TEXT_IN_ADJUSTMENTS = [-800, -639, -450, -250, -70, 10, 6, 3];
55
49
 
@@ -82,33 +76,6 @@ const TEXT_OUT_SCALES = [
82
76
  Vector(3, 0.2),
83
77
  ];
84
78
 
85
- /**
86
- * We do not actually need to render this sprite at all. It's only purpose is to be an invisible
87
- * reference for when and where we need to render the stage's name. Thus, we specify "false" for
88
- * "loadGraphics", but still manage playing its animations in the code below.
89
- */
90
- const topStreakSprite = Sprite();
91
-
92
- /**
93
- * We do not actually need to render this sprite at all. It's only purpose is to be an invisible
94
- * reference for when and where we need to render the stage's name. Thus, we specify "false" for
95
- * "loadGraphics", but still manage playing its animations in the code below.
96
- */
97
- const bottomStreakSprite = Sprite();
98
-
99
- /**
100
- * We must load the sprites in an init function to prevent issues with mods that replace the vanilla
101
- * files. (For some reason, loading the sprites will cause the overwrite to no longer apply on the
102
- * second and subsequent runs.)
103
- */
104
- export function streakTextInit(): void {
105
- topStreakSprite.Load("resources/gfx/ui/ui_streak.anm2", false);
106
- topStreakSprite.PlaybackSpeed = TEXT_PLAYBACK_SPEED;
107
-
108
- bottomStreakSprite.Load("resources/gfx/ui/ui_streak.anm2", false);
109
- bottomStreakSprite.PlaybackSpeed = TEXT_PLAYBACK_SPEED;
110
- }
111
-
112
79
  // ModCallback.POST_RENDER (2)
113
80
  export function streakTextPostRender(): void {
114
81
  // The top streak only plays when the player arrives on the floor (or continues a game from the
@@ -124,7 +91,7 @@ export function streakTextPostRender(): void {
124
91
  function checkEndTopStreakText() {
125
92
  if (
126
93
  v.run.topStreakTextStartedRenderFrame === null ||
127
- !topStreakSprite.IsPlaying(UIStreakAnimation.TEXT_STAY)
94
+ v.run.topStreakText.animation !== UIStreakAnimation.TEXT_STAY
128
95
  ) {
129
96
  return;
130
97
  }
@@ -133,7 +100,10 @@ function checkEndTopStreakText() {
133
100
  const elapsedFrames =
134
101
  renderFrameCount - v.run.topStreakTextStartedRenderFrame;
135
102
  if (elapsedFrames >= 115) {
136
- playTextOut(topStreakSprite);
103
+ v.run.topStreakText.animation = UIStreakAnimation.TEXT;
104
+ // We adjust by the frame backwards by an arbitrary amount to roughly align with the speed of
105
+ // the vanilla animation.
106
+ v.run.topStreakText.frame = TEXT_OUT_FRAME - 2;
137
107
  }
138
108
  }
139
109
 
@@ -165,7 +135,7 @@ function trackMapInputPressed() {
165
135
  * slides in from the left.
166
136
  */
167
137
  function checkStartBottomStreakText() {
168
- if (bottomStreakSprite.IsPlaying()) {
138
+ if (v.run.bottomStreakText.animation !== UIStreakAnimation.NONE) {
169
139
  return;
170
140
  }
171
141
 
@@ -180,7 +150,8 @@ function checkStartBottomStreakText() {
180
150
  const gameFrameCount = game.GetFrameCount();
181
151
  const elapsedFrames = gameFrameCount - earliestFrame;
182
152
  if (elapsedFrames >= NUM_RENDER_FRAMES_MAP_HELD_BEFORE_STREAK_TEXT) {
183
- bottomStreakSprite.Play(UIStreakAnimation.TEXT, true);
153
+ v.run.bottomStreakText.animation = UIStreakAnimation.TEXT;
154
+ v.run.bottomStreakText.frame = 0;
184
155
  }
185
156
  }
186
157
 
@@ -189,7 +160,7 @@ function checkStartBottomStreakText() {
189
160
  * right.
190
161
  */
191
162
  function checkEndBottomStreakText() {
192
- if (!bottomStreakSprite.IsPlaying(UIStreakAnimation.TEXT_STAY)) {
163
+ if (v.run.bottomStreakText.animation !== UIStreakAnimation.TEXT_STAY) {
193
164
  return;
194
165
  }
195
166
 
@@ -197,16 +168,13 @@ function checkEndBottomStreakText() {
197
168
  ...v.run.controllerIndexPushingMapRenderFrame.values(),
198
169
  ];
199
170
  if (pushedMapFrames.length === 0) {
200
- playTextOut(bottomStreakSprite);
171
+ v.run.bottomStreakText.animation = UIStreakAnimation.TEXT;
172
+ // We adjust by the frame backwards by an arbitrary amount to roughly align with the speed of
173
+ // the vanilla animation.
174
+ v.run.bottomStreakText.frame = TEXT_OUT_FRAME - 2;
201
175
  }
202
176
  }
203
177
 
204
- // ModCallback.POST_GAME_STARTED (15)
205
- export function streakTextPostGameStarted(): void {
206
- topStreakSprite.Stop();
207
- bottomStreakSprite.Stop();
208
- }
209
-
210
178
  // ModCallback.GET_SHADER_PARAMS (22)
211
179
  export function streakTextGetShaderParams(
212
180
  customStage: CustomStage,
@@ -218,27 +186,44 @@ export function streakTextGetShaderParams(
218
186
 
219
187
  const topCenterPos = getScreenTopCenterPos();
220
188
  const topStreakPosition = topCenterPos.add(STREAK_SPRITE_TOP_OFFSET);
221
- renderSprite(customStage, topStreakSprite, topStreakPosition);
189
+ renderStreakText(customStage, v.run.topStreakText, topStreakPosition);
222
190
 
223
191
  const bottomCenterPos = getScreenBottomCenterPos();
224
192
  const bottomStreakPosition = bottomCenterPos.add(STREAK_SPRITE_BOTTOM_OFFSET);
225
- renderSprite(customStage, bottomStreakSprite, bottomStreakPosition);
193
+ renderStreakText(customStage, v.run.bottomStreakText, bottomStreakPosition);
226
194
  }
227
195
 
228
- function renderSprite(
196
+ function renderStreakText(
229
197
  customStage: CustomStage,
230
- sprite: Sprite,
198
+ streakText: { animation: UIStreakAnimation; frame: int; pauseFrame: boolean },
231
199
  position: Vector,
232
200
  ) {
233
- sprite.Update();
234
- if (!sprite.IsPlaying()) {
201
+ if (streakText.animation === UIStreakAnimation.NONE) {
202
+ return;
203
+ }
204
+
205
+ if (streakText.animation !== UIStreakAnimation.TEXT_STAY) {
206
+ const { pauseFrame } = streakText;
207
+ streakText.pauseFrame = !streakText.pauseFrame;
208
+
209
+ if (!pauseFrame) {
210
+ streakText.frame++;
211
+ }
212
+ }
213
+
214
+ const endFrame = UI_STREAK_ANIMATION_END_FRAMES[streakText.animation];
215
+ if (streakText.frame > endFrame) {
216
+ streakText.animation = UIStreakAnimation.NONE;
217
+ streakText.frame = 0;
235
218
  return;
236
219
  }
237
220
 
238
- const animation = sprite.GetAnimation() as UIStreakAnimation;
239
- const frame = sprite.GetFrame();
240
- if (animation === UIStreakAnimation.TEXT && frame === TEXT_STAY_FRAME) {
241
- sprite.Play(UIStreakAnimation.TEXT_STAY, true);
221
+ if (
222
+ streakText.animation === UIStreakAnimation.TEXT &&
223
+ streakText.frame === TEXT_STAY_FRAME
224
+ ) {
225
+ streakText.animation = UIStreakAnimation.TEXT_STAY;
226
+ streakText.frame = 0;
242
227
  }
243
228
 
244
229
  const isPaused = game.IsPaused();
@@ -248,39 +233,22 @@ function renderSprite(
248
233
 
249
234
  const font = fonts.upheaval;
250
235
  const { name } = customStage;
251
- const length = font.GetStringWidthUTF8(name);
236
+ const numberSuffix = v.run.firstFloor ? "I" : "II";
237
+ const nameWithNumberSuffix = `${name} ${numberSuffix}`;
238
+ const length = font.GetStringWidthUTF8(nameWithNumberSuffix);
252
239
  const centeredX = position.X - length / 2;
253
240
 
254
241
  let adjustment = 0;
255
242
  let scale = VectorOne;
256
- switch (animation) {
257
- case UIStreakAnimation.TEXT: {
258
- if (frame < TEXT_STAY_FRAME) {
259
- adjustment = TEXT_IN_ADJUSTMENTS[frame] ?? 0;
260
- scale = TEXT_IN_SCALES[frame] ?? VectorOne;
261
- } else {
262
- const adjustedFrame = frame - TEXT_OUT_FRAME;
263
- adjustment = TEXT_OUT_ADJUSTMENTS[adjustedFrame] ?? 0;
264
- scale = TEXT_OUT_SCALES[adjustedFrame] ?? VectorOne;
265
- }
266
243
 
267
- break;
268
- }
269
-
270
- case UIStreakAnimation.TEXT_IN: {
271
- adjustment = TEXT_IN_ADJUSTMENTS[frame] ?? 0;
272
- scale = TEXT_IN_SCALES[frame] ?? VectorOne;
273
- break;
274
- }
275
-
276
- case UIStreakAnimation.TEXT_OUT: {
277
- adjustment = TEXT_OUT_ADJUSTMENTS[frame] ?? 0;
278
- scale = TEXT_OUT_SCALES[frame] ?? VectorOne;
279
- break;
280
- }
281
-
282
- default: {
283
- break;
244
+ if (streakText.animation === UIStreakAnimation.TEXT) {
245
+ if (streakText.frame < TEXT_STAY_FRAME) {
246
+ adjustment = TEXT_IN_ADJUSTMENTS[streakText.frame] ?? 0;
247
+ scale = TEXT_IN_SCALES[streakText.frame] ?? VectorOne;
248
+ } else {
249
+ const adjustedFrame = streakText.frame - TEXT_OUT_FRAME;
250
+ adjustment = TEXT_OUT_ADJUSTMENTS[adjustedFrame] ?? 0;
251
+ scale = TEXT_OUT_SCALES[adjustedFrame] ?? VectorOne;
284
252
  }
285
253
  }
286
254
 
@@ -288,7 +256,7 @@ function renderSprite(
288
256
  const adjustedY = position.Y + STREAK_TEXT_BOTTOM_Y_OFFSET;
289
257
 
290
258
  font.DrawStringScaled(
291
- name,
259
+ nameWithNumberSuffix,
292
260
  adjustedX,
293
261
  adjustedY,
294
262
  scale.X,
@@ -297,12 +265,6 @@ function renderSprite(
297
265
  );
298
266
  }
299
267
 
300
- function playTextOut(sprite: Sprite) {
301
- sprite.Play(UIStreakAnimation.TEXT, true);
302
- // We adjust by 2 to roughly align with the speed of the vanilla animation.
303
- sprite.SetFrame(TEXT_OUT_FRAME - 2);
304
- }
305
-
306
268
  export function topStreakTextStart(): void {
307
269
  const level = game.GetLevel();
308
270
  const renderFrameCount = Isaac.GetFrameCount();
@@ -311,6 +273,7 @@ export function topStreakTextStart(): void {
311
273
  level.ShowName(false);
312
274
 
313
275
  // Initiate the animation for the custom text.
276
+ v.run.topStreakText.animation = UIStreakAnimation.TEXT;
277
+ v.run.topStreakText.frame = 0;
314
278
  v.run.topStreakTextStartedRenderFrame = renderFrameCount;
315
- topStreakSprite.Play(UIStreakAnimation.TEXT, true);
316
279
  }
@@ -1,15 +1,32 @@
1
1
  import { ControllerIndex } from "isaac-typescript-definitions";
2
2
  import { CustomStage } from "../../interfaces/CustomStage";
3
+ import { UIStreakAnimation } from "./customStageConstants";
3
4
 
4
5
  const v = {
5
6
  run: {
6
7
  currentCustomStage: null as CustomStage | null,
8
+
9
+ /** Whether we are on e.g. Caves 1 or Caves 2. */
10
+ firstFloor: true,
11
+
7
12
  showingBossVersusScreen: false,
8
13
 
9
14
  /** Values are the render frame that the controller first pressed the map button. */
10
15
  controllerIndexPushingMapRenderFrame: new Map<ControllerIndex, int>(),
11
16
 
12
17
  topStreakTextStartedRenderFrame: null as int | null,
18
+
19
+ topStreakText: {
20
+ animation: UIStreakAnimation.NONE,
21
+ frame: 0,
22
+ pauseFrame: false,
23
+ },
24
+
25
+ bottomStreakText: {
26
+ animation: UIStreakAnimation.NONE,
27
+ frame: 0,
28
+ pauseFrame: false,
29
+ },
13
30
  },
14
31
 
15
32
  room: {