isaacscript-common 7.5.0 → 7.6.1

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 (86) hide show
  1. package/dist/classes/DefaultMap.d.ts +3 -2
  2. package/dist/classes/DefaultMap.d.ts.map +1 -1
  3. package/dist/classes/DefaultMap.lua +2 -1
  4. package/dist/features/customGridEntity.d.ts +6 -3
  5. package/dist/features/customGridEntity.d.ts.map +1 -1
  6. package/dist/features/customGridEntity.lua +20 -11
  7. package/dist/features/customStage/customStageConstants.d.ts +1 -0
  8. package/dist/features/customStage/customStageConstants.d.ts.map +1 -1
  9. package/dist/features/customStage/customStageConstants.lua +1 -0
  10. package/dist/features/customStage/customStageGridEntities.d.ts.map +1 -1
  11. package/dist/features/customStage/customStageGridEntities.lua +9 -5
  12. package/dist/features/customStage/customStageUtils.d.ts +2 -1
  13. package/dist/features/customStage/customStageUtils.d.ts.map +1 -1
  14. package/dist/features/customStage/customStageUtils.lua +40 -1
  15. package/dist/features/customStage/exports.d.ts +1 -25
  16. package/dist/features/customStage/exports.d.ts.map +1 -1
  17. package/dist/features/customStage/exports.lua +28 -29
  18. package/dist/features/customStage/init.d.ts.map +1 -1
  19. package/dist/features/customStage/init.lua +3 -1
  20. package/dist/features/customStage/v.d.ts +0 -2
  21. package/dist/features/customStage/v.d.ts.map +1 -1
  22. package/dist/features/customStage/v.lua +0 -2
  23. package/dist/features/customStage/versusScreen.d.ts.map +1 -1
  24. package/dist/features/customStage/versusScreen.lua +80 -60
  25. package/dist/features/customTrapdoor/exports.d.ts +28 -16
  26. package/dist/features/customTrapdoor/exports.d.ts.map +1 -1
  27. package/dist/features/customTrapdoor/exports.lua +45 -61
  28. package/dist/features/customTrapdoor/init.d.ts.map +1 -1
  29. package/dist/features/customTrapdoor/init.lua +12 -10
  30. package/dist/features/customTrapdoor/spawn.d.ts +6 -0
  31. package/dist/features/customTrapdoor/spawn.d.ts.map +1 -0
  32. package/dist/features/customTrapdoor/spawn.lua +51 -0
  33. package/dist/features/customTrapdoor/v.d.ts +2 -2
  34. package/dist/features/customTrapdoor/v.d.ts.map +1 -1
  35. package/dist/functions/doors.d.ts +6 -5
  36. package/dist/functions/doors.d.ts.map +1 -1
  37. package/dist/functions/doors.lua +25 -12
  38. package/dist/functions/enums.d.ts +3 -3
  39. package/dist/functions/enums.lua +3 -3
  40. package/dist/functions/players.d.ts.map +1 -1
  41. package/dist/functions/players.lua +3 -2
  42. package/dist/index.d.ts +172 -11145
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.lua +4 -4
  45. package/dist/interfaces/{CustomStageLua.d.ts → CustomStageTSConfig.d.ts} +87 -61
  46. package/dist/interfaces/CustomStageTSConfig.d.ts.map +1 -0
  47. package/dist/interfaces/{CustomStageLua.lua → CustomStageTSConfig.lua} +0 -0
  48. package/dist/interfaces/GridEntityCustomData.d.ts +2 -2
  49. package/dist/interfaces/GridEntityCustomData.d.ts.map +1 -1
  50. package/dist/interfaces/JSONRoomsFile.d.ts +6 -5
  51. package/dist/interfaces/JSONRoomsFile.d.ts.map +1 -1
  52. package/dist/interfaces/private/CustomStage.d.ts +1 -1
  53. package/dist/interfaces/private/CustomStage.d.ts.map +1 -1
  54. package/dist/interfaces/private/CustomTrapdoorDescription.d.ts +2 -2
  55. package/dist/interfaces/private/CustomTrapdoorDescription.d.ts.map +1 -1
  56. package/dist/interfaces/private/CustomTrapdoorDestination.d.ts +14 -0
  57. package/dist/interfaces/private/CustomTrapdoorDestination.d.ts.map +1 -0
  58. package/dist/{types/TrapdoorDestination.lua → interfaces/private/CustomTrapdoorDestination.lua} +0 -0
  59. package/package.json +1 -1
  60. package/src/classes/DefaultMap.ts +3 -2
  61. package/src/features/customGridEntity.ts +30 -20
  62. package/src/features/customStage/customStageConstants.ts +2 -0
  63. package/src/features/customStage/customStageGridEntities.ts +20 -16
  64. package/src/features/customStage/customStageUtils.ts +52 -1
  65. package/src/features/customStage/exports.ts +33 -45
  66. package/src/features/customStage/init.ts +3 -2
  67. package/src/features/customStage/v.ts +0 -6
  68. package/src/features/customStage/versusScreen.ts +78 -57
  69. package/src/features/customTrapdoor/exports.ts +60 -66
  70. package/src/features/customTrapdoor/init.ts +12 -11
  71. package/src/features/customTrapdoor/spawn.ts +53 -0
  72. package/src/features/customTrapdoor/v.ts +2 -2
  73. package/src/functions/doors.ts +37 -21
  74. package/src/functions/enums.ts +3 -3
  75. package/src/functions/players.ts +7 -3
  76. package/src/index.ts +3 -4
  77. package/src/interfaces/{CustomStageLua.ts → CustomStageTSConfig.ts} +108 -42
  78. package/src/interfaces/GridEntityCustomData.ts +2 -2
  79. package/src/interfaces/JSONRoomsFile.ts +6 -5
  80. package/src/interfaces/private/CustomStage.ts +4 -1
  81. package/src/interfaces/private/CustomTrapdoorDescription.ts +2 -2
  82. package/src/interfaces/private/CustomTrapdoorDestination.ts +14 -0
  83. package/dist/interfaces/CustomStageLua.d.ts.map +0 -1
  84. package/dist/types/TrapdoorDestination.d.ts +0 -6
  85. package/dist/types/TrapdoorDestination.d.ts.map +0 -1
  86. package/src/types/TrapdoorDestination.ts +0 -8
@@ -16,9 +16,11 @@ import {
16
16
  import { calculateStageType } from "../../functions/stage";
17
17
  import { vectorEquals } from "../../functions/vector";
18
18
  import { CustomStage } from "../../interfaces/private/CustomStage";
19
- import { TrapdoorDestination } from "../../types/TrapdoorDestination";
20
19
  import { isCustomGridEntity } from "../customGridEntity";
21
- import { spawnCustomTrapdoor } from "../customTrapdoor/exports";
20
+ import {
21
+ spawnCustomTrapdoor,
22
+ spawnCustomTrapdoorToVanilla,
23
+ } from "../customTrapdoor/exports";
22
24
  import { DEFAULT_BASE_STAGE } from "./exports";
23
25
  import v from "./v";
24
26
 
@@ -203,20 +205,22 @@ export function convertVanillaTrapdoors(
203
205
 
204
206
  removeGridEntity(gridEntity, true);
205
207
 
206
- // - If we are on the first floor of a custom stage, then the destination will be the second floor
207
- // of the custom stage. (e.g. Caves 1 to Caves 2)
208
- // - If we are on the second floor of a custom stage, then the destination will be the vanilla
209
- // floor equivalent to 2 floors after the floor used as a basis for the custom stage.
210
- const baseStage =
211
- customStage.baseStage === undefined
212
- ? DEFAULT_BASE_STAGE
213
- : customStage.baseStage;
214
- const vanillaNextStage = (baseStage + 2) as LevelStage;
215
- const vanillaNextStageType = calculateStageType(vanillaNextStage);
216
- const destination: TrapdoorDestination = v.run.firstFloor
217
- ? [customStage.name, 2]
218
- : [vanillaNextStage, vanillaNextStageType];
219
- spawnCustomTrapdoor(gridEntity.Position, destination);
208
+ if (v.run.firstFloor) {
209
+ // If we are on the first floor of a custom stage, then the destination will be the second floor
210
+ // of the custom stage. (e.g. Caves 1 to Caves 2)
211
+ spawnCustomTrapdoor(gridEntity.Position, customStage.name, 2);
212
+ } else {
213
+ // If we are on the second floor of a custom stage, then the destination will be the vanilla
214
+ // floor equivalent to 2 floors after the floor used as a basis for the custom stage.
215
+ const baseStage =
216
+ customStage.baseStage === undefined
217
+ ? DEFAULT_BASE_STAGE
218
+ : customStage.baseStage;
219
+ const stage = (baseStage + 2) as LevelStage;
220
+ const stageType = calculateStageType(stage);
221
+
222
+ spawnCustomTrapdoorToVanilla(gridEntity.Position, stage, stageType);
223
+ }
220
224
  }
221
225
 
222
226
  /**
@@ -2,7 +2,10 @@ import { sumArray } from "../../functions/array";
2
2
  import { log } from "../../functions/log";
3
3
  import { getRandomFloat } from "../../functions/random";
4
4
  import { getRandomSeed } from "../../functions/rng";
5
- import { CustomStageRoomMetadata } from "../../interfaces/CustomStageLua";
5
+ import {
6
+ CustomStageBossPoolEntry,
7
+ CustomStageRoomMetadata,
8
+ } from "../../interfaces/CustomStageTSConfig";
6
9
 
7
10
  /**
8
11
  * Helper function to get a random custom stage room from an array of custom stage rooms.
@@ -52,3 +55,51 @@ function getCustomStageRoomWithChosenWeight(
52
55
  `Failed to get a custom stage room with chosen weight: ${chosenWeight}`,
53
56
  );
54
57
  }
58
+
59
+ export function getRandomBossRoomFromPool(
60
+ roomsMetadata: readonly CustomStageRoomMetadata[],
61
+ bossPool: readonly CustomStageBossPoolEntry[],
62
+ seedOrRNG: Seed | RNG = getRandomSeed(),
63
+ verbose = false,
64
+ ): CustomStageRoomMetadata {
65
+ const totalWeight = getTotalWeightOfBossPool(bossPool);
66
+ if (verbose) {
67
+ log(`Total weight of the custom stage boss pool provided: ${totalWeight}`);
68
+ }
69
+
70
+ const chosenWeight = getRandomFloat(0, totalWeight, seedOrRNG);
71
+ if (verbose) {
72
+ log(`Randomly chose weight for custom stage boss pool: ${chosenWeight}`);
73
+ }
74
+
75
+ const bossEntry = getBossEntryWithChosenWeight(bossPool, chosenWeight);
76
+
77
+ const roomsMetadataForBoss = roomsMetadata.filter(
78
+ (roomMetadata) => roomMetadata.subType === bossEntry.subType,
79
+ );
80
+ return getRandomCustomStageRoom(roomsMetadataForBoss, seedOrRNG, verbose);
81
+ }
82
+
83
+ function getTotalWeightOfBossPool(
84
+ bossPool: readonly CustomStageBossPoolEntry[],
85
+ ): float {
86
+ const weights = bossPool.map((bossEntry) => bossEntry.weight);
87
+ return sumArray(weights);
88
+ }
89
+
90
+ function getBossEntryWithChosenWeight(
91
+ bossPool: readonly CustomStageBossPoolEntry[],
92
+ chosenWeight: float,
93
+ ): CustomStageBossPoolEntry {
94
+ for (const bossEntry of bossPool) {
95
+ if (chosenWeight < bossEntry.weight) {
96
+ return bossEntry;
97
+ }
98
+
99
+ chosenWeight -= bossEntry.weight;
100
+ }
101
+
102
+ error(
103
+ `Failed to get a custom stage boss entry with chosen weight: ${chosenWeight}`,
104
+ );
105
+ }
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import {
11
- EntityType,
11
+ DoorSlot,
12
12
  LevelStage,
13
13
  RoomShape,
14
14
  RoomType,
@@ -16,7 +16,7 @@ import {
16
16
  } from "isaac-typescript-definitions";
17
17
  import { reorderedCallbacksSetStageInternal } from "../../callbacks/reorderedCallbacks";
18
18
  import { game } from "../../core/cachedClasses";
19
- import { getEntityIDFromConstituents } from "../../functions/entities";
19
+ import { doorSlotFlagsToDoorSlots } from "../../functions/doors";
20
20
  import { logError } from "../../functions/log";
21
21
  import { newRNG } from "../../functions/rng";
22
22
  import {
@@ -25,13 +25,13 @@ import {
25
25
  } from "../../functions/rooms";
26
26
  import { setStage } from "../../functions/stage";
27
27
  import { asNumber } from "../../functions/types";
28
+ import { CustomStageRoomMetadata } from "../../interfaces/CustomStageTSConfig";
28
29
  import { CustomStage } from "../../interfaces/private/CustomStage";
29
- import { getRandomCustomStageRoom } from "./customStageUtils";
30
- import v, {
31
- customBossPNGPaths,
32
- customStageCachedRoomData,
33
- customStagesMap,
34
- } from "./v";
30
+ import {
31
+ getRandomBossRoomFromPool,
32
+ getRandomCustomStageRoom,
33
+ } from "./customStageUtils";
34
+ import v, { customStageCachedRoomData, customStagesMap } from "./v";
35
35
 
36
36
  export const DEFAULT_BASE_STAGE = LevelStage.BASEMENT_2;
37
37
  export const DEFAULT_BASE_STAGE_TYPE = StageType.ORIGINAL;
@@ -135,8 +135,8 @@ function setStageRoomsData(
135
135
  const roomType = room.Data.Type;
136
136
  const roomShapeMap = customStage.roomTypeMap.get(roomType);
137
137
  if (roomShapeMap === undefined) {
138
- // Only show errors for non-default room types. (By default, we won't replace shops and other
139
- // special rooms.)
138
+ // Only show errors for non-default room types. (We do not require that end-users provide
139
+ // custom rooms for shops and other special rooms inside of their XML file.)
140
140
  if (roomType === RoomType.DEFAULT) {
141
141
  logError(
142
142
  `Failed to find any custom rooms for RoomType.${RoomType[roomType]} (${roomType}) for custom stage: ${customStage.name}`,
@@ -160,10 +160,32 @@ function setStageRoomsData(
160
160
  logError(
161
161
  `Failed to find any custom rooms for RoomType.${RoomType[roomType]} (${roomType}) + RoomShape.${RoomShape[roomShape]} (${roomShape}) + DoorSlotFlags ${doorSlotFlags} for custom stage: ${customStage.name}`,
162
162
  );
163
+
164
+ const header = `For reference, a DoorSlotFlags of ${doorSlotFlags} is equal to the following doors being enabled:\n`;
165
+ const doorSlots = doorSlotFlagsToDoorSlots(doorSlotFlags);
166
+ const doorSlotLines = doorSlots.map(
167
+ (doorSlot) => `- DoorSlot.${DoorSlot[doorSlot]} (${doorSlot})`,
168
+ );
169
+ const explanation = header + doorSlotLines.join("\n");
170
+ logError(explanation);
163
171
  continue;
164
172
  }
165
173
 
166
- const randomRoom = getRandomCustomStageRoom(roomsMetadata, rng, verbose);
174
+ let randomRoom: CustomStageRoomMetadata;
175
+ if (roomType === RoomType.BOSS) {
176
+ if (customStage.bossPool === undefined) {
177
+ continue;
178
+ }
179
+
180
+ randomRoom = getRandomBossRoomFromPool(
181
+ roomsMetadata,
182
+ customStage.bossPool,
183
+ rng,
184
+ verbose,
185
+ );
186
+ } else {
187
+ randomRoom = getRandomCustomStageRoom(roomsMetadata, rng, verbose);
188
+ }
167
189
 
168
190
  let newRoomData = customStageCachedRoomData.get(randomRoom.variant);
169
191
  if (newRoomData === undefined) {
@@ -187,40 +209,6 @@ function setStageRoomsData(
187
209
  }
188
210
  }
189
211
 
190
- /**
191
- * By default, unknown bosses will be drawn on the boss "versus" screen as "???". If your custom
192
- * stage has custom bosses, you can use this function to register the corresponding graphic file
193
- * files for them.
194
- *
195
- * For reference:
196
- * - The vanilla name sprite for Monstro is located at:
197
- * `resources/gfx/ui/boss/bossname_20.0_monstro.png`
198
- * - The vanilla portrait sprite for Monstro is located at:
199
- * `resources/gfx/ui/boss/portrait_20.0_monstro.png`
200
- *
201
- * (Note that boss metadata like this cannot be specified with the rest of the custom stage metadata
202
- * in the "tsconfig.json" file because there is not a way to retrieve the name of an entity at
203
- * run-time.)
204
- *
205
- * @param entityType The entity type of the custom boss.
206
- * @param variant The variant of the custom boss.
207
- * @param subType The sub-type of the custom boss.
208
- * @param namePNGPath The full path to the PNG file that contains the name of the boss that will be
209
- * displayed on the top of the boss "versus" screen.
210
- * @param portraitPNGPath The full path to the PNG file that contains the portrait of the boss that
211
- * will be displayed on the right side of the boss "versus" screen.
212
- */
213
- export function registerCustomBoss(
214
- entityType: EntityType,
215
- variant: int,
216
- subType: int,
217
- namePNGPath: string,
218
- portraitPNGPath: string,
219
- ): void {
220
- const entityID = getEntityIDFromConstituents(entityType, variant, subType);
221
- customBossPNGPaths.set(entityID, [namePNGPath, portraitPNGPath]);
222
- }
223
-
224
212
  /**
225
213
  * Helper function to disable the custom stage. This is typically called before taking the player to
226
214
  * a vanilla floor.
@@ -13,10 +13,11 @@ import { hasFlag, removeFlag } from "../../functions/flag";
13
13
  import {
14
14
  CustomStageLua,
15
15
  CustomStageRoomMetadata,
16
- } from "../../interfaces/CustomStageLua";
16
+ } from "../../interfaces/CustomStageTSConfig";
17
17
  import { CustomStage, RoomTypeMap } from "../../interfaces/private/CustomStage";
18
18
  import { saveDataManager } from "../saveDataManager/exports";
19
19
  import { setCustomStageBackdrop } from "./backdrop";
20
+ import { CUSTOM_STAGE_FEATURE_NAME } from "./customStageConstants";
20
21
  import {
21
22
  convertVanillaTrapdoors,
22
23
  removeUrnRewards,
@@ -42,7 +43,7 @@ export function customStageInit(mod: ModUpgraded): void {
42
43
  return;
43
44
  }
44
45
 
45
- saveDataManager("customStage", v);
46
+ saveDataManager(CUSTOM_STAGE_FEATURE_NAME, v);
46
47
  versusScreenInit();
47
48
 
48
49
  mod.AddCallback(ModCallback.POST_RENDER, postRender); // 2
@@ -40,9 +40,3 @@ export const customStagesMap = new Map<string, CustomStage>();
40
40
 
41
41
  /** Indexed by room variant. */
42
42
  export const customStageCachedRoomData = new Map<int, Readonly<RoomConfig>>();
43
-
44
- /** Indexed by entity ID. */
45
- export const customBossPNGPaths = new Map<
46
- string,
47
- [namePNGPath: string, portraitPNGPath: string]
48
- >();
@@ -9,7 +9,7 @@ import {
9
9
  import { game, sfxManager } from "../../core/cachedClasses";
10
10
  import { arrayRemove } from "../../functions/array";
11
11
  import { getBosses } from "../../functions/bosses";
12
- import { getEntityID } from "../../functions/entities";
12
+ import { getRoomSubType } from "../../functions/roomData";
13
13
  import { erange } from "../../functions/utils";
14
14
  import { CustomStage } from "../../interfaces/private/CustomStage";
15
15
  import { BOSS_NAME_PNG_FILE_NAMES } from "../../objects/bossNamePNGFileNames";
@@ -18,15 +18,19 @@ import { PLAYER_NAME_PNG_FILE_NAMES } from "../../objects/playerNamePNGFileNames
18
18
  import { PLAYER_PORTRAIT_PNG_FILE_NAMES } from "../../objects/playerPortraitPNGFileNames";
19
19
  import { VERSUS_SCREEN_BACKGROUND_COLORS } from "../../objects/versusScreenBackgroundColors";
20
20
  import { VERSUS_SCREEN_DIRT_SPOT_COLORS } from "../../objects/versusScreenDirtSpotColors";
21
+ import { disableAllSound, enableAllSound } from "../disableAllSound";
21
22
  import { pause, unpause } from "../pause";
22
23
  import { runNextGameFrame } from "../runInNFrames";
23
- import { ISAACSCRIPT_CUSTOM_STAGE_GFX_PATH } from "./customStageConstants";
24
+ import {
25
+ CUSTOM_STAGE_FEATURE_NAME,
26
+ ISAACSCRIPT_CUSTOM_STAGE_GFX_PATH,
27
+ } from "./customStageConstants";
24
28
  import {
25
29
  DEFAULT_BASE_STAGE,
26
30
  DEFAULT_BASE_STAGE_TYPE,
27
31
  INVALID_STAGE_VALUE,
28
32
  } from "./exports";
29
- import v, { customBossPNGPaths } from "./v";
33
+ import v from "./v";
30
34
 
31
35
  const DEFAULT_CHARACTER = PlayerType.ISAAC;
32
36
  const DEFAULT_STAGE_ID = StageID.BASEMENT;
@@ -136,23 +140,27 @@ export function playVersusScreenAnimation(customStage: CustomStage): void {
136
140
 
137
141
  pause();
138
142
  hud.SetVisible(false);
143
+ disableAllSound(CUSTOM_STAGE_FEATURE_NAME);
144
+
145
+ // Player
146
+ {
147
+ const { namePNGPath, portraitPNGPath } = getPlayerPNGPaths();
148
+ versusScreenSprite.ReplaceSpritesheet(PLAYER_NAME_ANM2_LAYER, namePNGPath);
149
+ versusScreenSprite.ReplaceSpritesheet(
150
+ PLAYER_PORTRAIT_ANM2_LAYER,
151
+ portraitPNGPath,
152
+ );
153
+ }
139
154
 
140
- const [playerNamePNGPath, playerPortraitPNGPath] = getPlayerPNGPaths();
141
- versusScreenSprite.ReplaceSpritesheet(
142
- PLAYER_NAME_ANM2_LAYER,
143
- playerNamePNGPath,
144
- );
145
- versusScreenSprite.ReplaceSpritesheet(
146
- PLAYER_PORTRAIT_ANM2_LAYER,
147
- playerPortraitPNGPath,
148
- );
149
-
150
- const [bossNamePNGPath, bossPortraitPNGPath] = getBossPNGPaths();
151
- versusScreenSprite.ReplaceSpritesheet(BOSS_NAME_ANM2_LAYER, bossNamePNGPath);
152
- versusScreenSprite.ReplaceSpritesheet(
153
- BOSS_PORTRAIT_ANM2_LAYER,
154
- bossPortraitPNGPath,
155
- );
155
+ // Boss
156
+ {
157
+ const { namePNGPath, portraitPNGPath } = getBossPNGPaths(customStage);
158
+ versusScreenSprite.ReplaceSpritesheet(BOSS_NAME_ANM2_LAYER, namePNGPath);
159
+ versusScreenSprite.ReplaceSpritesheet(
160
+ BOSS_PORTRAIT_ANM2_LAYER,
161
+ portraitPNGPath,
162
+ );
163
+ }
156
164
 
157
165
  versusScreenSprite.LoadGraphics();
158
166
 
@@ -186,67 +194,81 @@ function willVanillaVersusScreenPlay() {
186
194
  }
187
195
 
188
196
  /** Use the character of the 0th player. */
189
- function getPlayerPNGPaths(): [
190
- playerNamePNGPath: string,
191
- playerPortraitPNGPath: string,
192
- ] {
197
+ function getPlayerPNGPaths(): {
198
+ namePNGPath: string;
199
+ portraitPNGPath: string;
200
+ } {
193
201
  const player = Isaac.GetPlayer();
194
202
  const character = player.GetPlayerType();
195
203
 
196
- let playerNamePNGFileName = PLAYER_NAME_PNG_FILE_NAMES[character];
197
- if (playerNamePNGFileName === undefined) {
198
- playerNamePNGFileName = PLAYER_NAME_PNG_FILE_NAMES[DEFAULT_CHARACTER];
204
+ let namePNGFileName = PLAYER_NAME_PNG_FILE_NAMES[character];
205
+ if (namePNGFileName === undefined) {
206
+ namePNGFileName = PLAYER_NAME_PNG_FILE_NAMES[DEFAULT_CHARACTER];
199
207
  }
200
208
 
201
- const playerNamePNGPath = `${PNG_PATH_PREFIX}/${playerNamePNGFileName}`;
209
+ const namePNGPath = `${PNG_PATH_PREFIX}/${namePNGFileName}`;
202
210
 
203
- let playerPortraitFileName = PLAYER_PORTRAIT_PNG_FILE_NAMES[character];
204
- if (playerNamePNGFileName === undefined) {
205
- playerPortraitFileName = PLAYER_PORTRAIT_PNG_FILE_NAMES[DEFAULT_CHARACTER];
211
+ let portraitFileName = PLAYER_PORTRAIT_PNG_FILE_NAMES[character];
212
+ if (namePNGFileName === undefined) {
213
+ portraitFileName = PLAYER_PORTRAIT_PNG_FILE_NAMES[DEFAULT_CHARACTER];
206
214
  }
207
215
 
208
- const playerPortraitPNGPath = `${PLAYER_PORTRAIT_PNG_PATH_PREFIX}/${playerPortraitFileName}`;
216
+ const portraitPNGPath = `${PLAYER_PORTRAIT_PNG_PATH_PREFIX}/${portraitFileName}`;
209
217
 
210
- return [playerNamePNGPath, playerPortraitPNGPath];
218
+ return { namePNGPath, portraitPNGPath };
211
219
  }
212
220
 
213
221
  /** Use the boss of the first boss found. */
214
- function getBossPNGPaths(): [
215
- bossNamePNGPath: string,
216
- bossPortraitPNGPath: string,
217
- ] {
218
- const bosses = getBosses();
219
- const firstBoss = bosses[0];
220
-
222
+ function getBossPNGPaths(customStage: CustomStage): {
223
+ namePNGPath: string;
224
+ portraitPNGPath: string;
225
+ } {
221
226
  // Prefer the PNG paths specified by the end-user, if any.
222
- if (firstBoss !== undefined) {
223
- const entityID = getEntityID(firstBoss);
224
- const pngPaths = customBossPNGPaths.get(entityID);
225
- if (pngPaths !== undefined) {
226
- return pngPaths;
227
- }
227
+ const paths = getBossPNGPathsCustom(customStage);
228
+ if (paths !== undefined) {
229
+ return paths;
228
230
  }
229
231
 
230
232
  // If this is not a vanilla boss, default to showing question marks.
233
+ const bosses = getBosses();
234
+ const firstBoss = bosses[0];
231
235
  const bossID = firstBoss === undefined ? 0 : firstBoss.GetBossID();
232
236
  if (bossID === 0) {
233
237
  const questionMarkSprite = `${PNG_PATH_PREFIX}/${
234
238
  BOSS_NAME_PNG_FILE_NAMES[BossID.BLUE_BABY]
235
239
  }`;
236
- const bossNamePNGPath = questionMarkSprite;
237
- const bossPortraitPNGPath = questionMarkSprite;
238
- return [bossNamePNGPath, bossPortraitPNGPath];
240
+ const namePNGPath = questionMarkSprite;
241
+ const portraitPNGPath = questionMarkSprite;
242
+ return { namePNGPath, portraitPNGPath };
239
243
  }
240
244
 
241
245
  // If this is a vanilla boss, it will have a boss ID, and we can use the corresponding vanilla
242
246
  // files.
243
- const bossNamePNGFileName = BOSS_NAME_PNG_FILE_NAMES[bossID];
244
- const bossNamePNGPath = `${PNG_PATH_PREFIX}/${bossNamePNGFileName}`;
247
+ const namePNGFileName = BOSS_NAME_PNG_FILE_NAMES[bossID];
248
+ const namePNGPath = `${PNG_PATH_PREFIX}/${namePNGFileName}`;
249
+
250
+ const portraitPNGFileName = BOSS_PORTRAIT_PNG_FILE_NAMES[bossID];
251
+ const portraitPNGPath = `${PNG_PATH_PREFIX}/${portraitPNGFileName}`;
252
+
253
+ return { namePNGPath, portraitPNGPath };
254
+ }
245
255
 
246
- const bossPortraitPNGFileName = BOSS_PORTRAIT_PNG_FILE_NAMES[bossID];
247
- const bossPortraitPNGPath = `${PNG_PATH_PREFIX}/${bossPortraitPNGFileName}`;
256
+ function getBossPNGPathsCustom(
257
+ customStage: CustomStage,
258
+ ): { namePNGPath: string; portraitPNGPath: string } | undefined {
259
+ if (customStage.bossPool === undefined) {
260
+ return undefined;
261
+ }
262
+
263
+ const roomSubType = getRoomSubType();
264
+ const matchingBossEntry = customStage.bossPool.find(
265
+ (bossEntry) => bossEntry.subType === roomSubType,
266
+ );
267
+ if (matchingBossEntry === undefined) {
268
+ return undefined;
269
+ }
248
270
 
249
- return [bossNamePNGPath, bossPortraitPNGPath];
271
+ return matchingBossEntry.versusScreen;
250
272
  }
251
273
 
252
274
  function finishVersusScreenAnimation() {
@@ -256,6 +278,7 @@ function finishVersusScreenAnimation() {
256
278
 
257
279
  unpause();
258
280
  hud.SetVisible(true);
281
+ enableAllSound(CUSTOM_STAGE_FEATURE_NAME);
259
282
 
260
283
  // The sound effect only plays once the versus cutscene is over.
261
284
  sfxManager.Play(SoundEffect.CASTLE_PORTCULLIS);
@@ -267,10 +290,8 @@ export function versusScreenPostRender(): void {
267
290
  return;
268
291
  }
269
292
 
270
- const isPaused = game.IsPaused();
271
- if (isPaused) {
272
- return;
273
- }
293
+ // We do not want to early return when the game is paused because we need to start displaying the
294
+ // black screen as soon as the slide animation starts.
274
295
 
275
296
  if (versusScreenSprite.IsFinished(VERSUS_SCREEN_ANIMATION_NAME)) {
276
297
  finishVersusScreenAnimation();
@@ -1,45 +1,28 @@
1
- import {
2
- GridCollisionClass,
3
- LevelStage,
4
- StageType,
5
- } from "isaac-typescript-definitions";
6
- import { game } from "../../core/cachedClasses";
7
- import { TrapdoorAnimation } from "../../enums/private/TrapdoorAnimation";
1
+ import { LevelStage, StageType } from "isaac-typescript-definitions";
8
2
  import { errorIfFeaturesNotInitialized } from "../../featuresInitialized";
9
3
  import { getNextStage, getNextStageType } from "../../functions/nextStage";
10
- import { getRoomListIndex } from "../../functions/roomData";
11
- import { isVector } from "../../functions/vector";
12
- import { CustomTrapdoorDescription } from "../../interfaces/private/CustomTrapdoorDescription";
13
- import { TrapdoorDestination } from "../../types/TrapdoorDestination";
14
- import { spawnCustomGridEntity } from "../customGridEntity";
15
- import {
16
- CUSTOM_TRAPDOOR_FEATURE_NAME,
17
- GridEntityTypeCustom,
18
- } from "./customTrapdoorConstants";
19
- import { shouldTrapdoorSpawnOpen } from "./openClose";
20
- import v from "./v";
4
+ import { CustomTrapdoorDestination } from "../../interfaces/private/CustomTrapdoorDestination";
5
+ import { CUSTOM_TRAPDOOR_FEATURE_NAME } from "./customTrapdoorConstants";
6
+ import { spawnCustomTrapdoorToDestination } from "./spawn";
21
7
 
22
8
  /**
23
- * Helper function to spawn a trapdoor grid entity that will have one or more of the following
24
- * attributes:
9
+ * Helper function to spawn a trapdoor grid entity that will take a player to a custom stage. If you
10
+ * want to create a custom trapdoor that goes to a vanilla stage instead, use the
11
+ * `spawnCustomTrapdoorToVanilla` helper function.
12
+ *
13
+ * Custom trapdoors can have one or more of the following attributes:
25
14
  *
26
15
  * - custom destination (or custom logic for after the player enters)
27
16
  * - custom graphics
28
17
  * - custom logic for opening/closing
29
18
  *
30
- * You can use this function to take the player to your custom stage.
31
- *
32
19
  * Under the hood, the custom trapdoor is represented by a decoration grid entity and is manually
33
20
  * respawned every time the player re-enters the room.
34
21
  *
35
22
  * @param gridIndexOrPosition The location in the room to spawn the trapdoor.
36
- * @param destination Optional. Used to specify where the player will go after jumping into the
37
- * trapdoor. Can either be a vanilla stage tuple, a custom stage tuple, or
38
- * undefined. For example, a destination of `[LevelStage.CAVES_1,
39
- * StageType.ORIGINAL]` corresponds to Caves 1, and a destination of
40
- * `["Slaughterhouse", 1]` corresponds to a custom stage of Slaughterhouse 1. If
41
- * the destination is undefined, then the "normal" destination corresponding to
42
- * the current stage and room will be used (e.g. the next floor, in most cases).
23
+ * @param customStageName The name of the custom stage.
24
+ * @param customStageFloorNum The floor of the custom stage. For most purposes, you should use 1 or
25
+ * 2.
43
26
  * @param anm2Path Optional. The path to the anm2 file to use. By default, the vanilla trapdoor anm2
44
27
  * of "gfx/grid/door_11_trapdoor.anm2" will be used. The specified anm2 file must
45
28
  * have animations called "Opened", "Closed", and "Open Animation".
@@ -48,54 +31,65 @@ import v from "./v";
48
31
  */
49
32
  export function spawnCustomTrapdoor(
50
33
  gridIndexOrPosition: int | Vector,
51
- destination?: TrapdoorDestination,
34
+ customStageName: string,
35
+ customStageFloorNum: int,
52
36
  anm2Path = "gfx/grid/door_11_trapdoor.anm2",
53
37
  spawnOpen?: boolean,
54
38
  ): GridEntity {
55
39
  errorIfFeaturesNotInitialized(CUSTOM_TRAPDOOR_FEATURE_NAME);
56
40
 
57
- const room = game.GetRoom();
58
- const roomFrameCount = room.GetFrameCount();
59
- const roomListIndex = getRoomListIndex();
60
- const gridIndex = isVector(gridIndexOrPosition)
61
- ? room.GetGridIndex(gridIndexOrPosition)
62
- : gridIndexOrPosition;
41
+ const destination: CustomTrapdoorDestination = {
42
+ customStageName,
43
+ customStageFloorNum,
44
+ };
63
45
 
64
- const gridEntity = spawnCustomGridEntity(
65
- GridEntityTypeCustom.TRAPDOOR_CUSTOM,
46
+ return spawnCustomTrapdoorToDestination(
66
47
  gridIndexOrPosition,
67
- GridCollisionClass.NONE,
48
+ destination,
68
49
  anm2Path,
69
- TrapdoorAnimation.OPENED,
50
+ spawnOpen,
70
51
  );
52
+ }
71
53
 
72
- const firstSpawn = roomFrameCount !== 0;
73
- const open =
74
- spawnOpen === undefined
75
- ? shouldTrapdoorSpawnOpen(gridEntity, firstSpawn)
76
- : spawnOpen;
77
- const destinationToUse =
78
- destination === undefined ? getDefaultDestination() : destination;
79
-
80
- const roomTrapdoorMap = v.level.trapdoors.getAndSetDefault(roomListIndex);
81
- const customTrapdoorDescription: CustomTrapdoorDescription = {
82
- open,
83
- destination: destinationToUse,
84
- firstSpawn,
85
- };
86
- roomTrapdoorMap.set(gridIndex, customTrapdoorDescription);
87
-
88
- if (!open) {
89
- const sprite = gridEntity.GetSprite();
90
- sprite.Play(TrapdoorAnimation.CLOSED, true);
91
- }
54
+ /**
55
+ * This is the same thing as the `spawnCustomTrapdoor` function, but instead of having a destination
56
+ * of a custom stage, it has a destination of a vanilla stage.
57
+ *
58
+ * For more information, see the `spawnCustomTrapdoor` function.
59
+ *
60
+ * @param gridIndexOrPosition The location in the room to spawn the trapdoor.
61
+ * @param stage Optional. The number of the vanilla stage to go to. If not provided, the "normal"
62
+ * next stage will be selected.
63
+ * @param stageType The stage type of the vanilla stage to go to. If not provided, the "normal" next
64
+ * stage type will be selected.
65
+ * @param anm2Path Optional. The path to the anm2 file to use. By default, the vanilla trapdoor anm2
66
+ * of "gfx/grid/door_11_trapdoor.anm2" will be used. The specified anm2 file must
67
+ * have animations called "Opened", "Closed", and "Open Animation".
68
+ * @param spawnOpen Optional. Whether or not to spawn the trapdoor in an open state. By default,
69
+ * behavior will be used that emulates a vanilla trapdoor.
70
+ */
71
+ export function spawnCustomTrapdoorToVanilla(
72
+ gridIndexOrPosition: int | Vector,
73
+ stage?: LevelStage,
74
+ stageType?: StageType,
75
+ anm2Path = "gfx/grid/door_11_trapdoor.anm2",
76
+ spawnOpen?: boolean,
77
+ ): GridEntity {
78
+ errorIfFeaturesNotInitialized(CUSTOM_TRAPDOOR_FEATURE_NAME);
92
79
 
93
- return gridEntity;
94
- }
80
+ const vanillaStage = stage === undefined ? getNextStage() : stage;
81
+ const vanillaStageType =
82
+ stageType === undefined ? getNextStageType() : stageType;
95
83
 
96
- function getDefaultDestination(): [stage: LevelStage, stageType: StageType] {
97
- const nextStage = getNextStage();
98
- const nextStageType = getNextStageType();
84
+ const destination: CustomTrapdoorDestination = {
85
+ vanillaStage,
86
+ vanillaStageType,
87
+ };
99
88
 
100
- return [nextStage, nextStageType];
89
+ return spawnCustomTrapdoorToDestination(
90
+ gridIndexOrPosition,
91
+ destination,
92
+ anm2Path,
93
+ spawnOpen,
94
+ );
101
95
  }