Skip to content

Commit f465ddb

Browse files
committed
EID: add helper for conditional item descriptions
1 parent 3310561 commit f465ddb

9 files changed

Lines changed: 147 additions & 38 deletions

File tree

src/Feature.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
import type { DamageFlag } from "isaac-typescript-definitions";
2-
import type { ModUpgraded } from "isaacscript-common";
32
import { ModFeature } from "isaacscript-common";
43
import type { EIDExtended } from "./compat/EID";
54

65
/** Abstract base class representing a custom feature. */
76
export abstract class Feature extends ModFeature {
8-
constructor(mod: ModUpgraded, init?: boolean) {
9-
super(mod, init);
10-
const ExEID = EID as EIDExtended | undefined;
11-
if (this.setupEID && ExEID) {
12-
this.setupEID(ExEID);
13-
}
14-
}
15-
167
onGameExit?(): void;
178

189
onGameStart?(isContinued: boolean): void;
@@ -158,6 +149,7 @@ export abstract class Feature extends ModFeature {
158149
*
159150
* @param eid The extended EID API instance used to register descriptions and modify how the item
160151
* appears in External Item Descriptions.
152+
* @param player The Player entity.
161153
*/
162-
setupEID?(eid: EIDExtended): void;
154+
setupEID?(eid: EIDExtended, player: EntityPlayer): void;
163155
}

src/characters/Character.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1+
import type { EIDExtended } from "../compat/EID";
12
import { Feature } from "../Feature";
23

34
export interface PlayerData {
45
fireDelayInit?: boolean;
56
}
67

78
/** Abstract base class representing a custom character. */
8-
export abstract class Character extends Feature {}
9+
export abstract class Character extends Feature {
10+
override postPlayerInit(player: EntityPlayer): void {
11+
const ExEID = EID as EIDExtended | undefined;
12+
if (this.setupEID && ExEID) {
13+
this.setupEID(ExEID, player);
14+
}
15+
}
16+
}

src/characters/Miku/MikuCharacter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ export class MikuCharacter extends Character {
6969
}
7070
}
7171

72+
@Callback(ModCallback.POST_PLAYER_INIT)
73+
override postPlayerInit(player: EntityPlayer): void {
74+
super.postPlayerInit(player);
75+
}
76+
7277
@Callback(ModCallback.EVALUATE_CACHE, CacheFlag.FIRE_DELAY)
7378
override cacheFireDelay(player: EntityPlayer): void {
7479
if (!isMiku(player)) {

src/characters/Miku/MikuTaintedCharacter.ts

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
} from "isaac-typescript-definitions";
1414
import type { SaveData } from "isaacscript-common";
1515
import {
16-
addCollectibleCostume,
1716
anyPlayerIs,
1817
Callback,
1918
CallbackCustom,
@@ -25,11 +24,11 @@ import {
2524
jsonEncode,
2625
ModCallbackCustom,
2726
ReadonlyMap,
28-
removeCollectibleCostume,
2927
spawnEffect,
3028
VectorZero,
3129
} from "isaacscript-common";
3230
import type { EIDExtended } from "../../compat/EID";
31+
import { appendToDescription } from "../../compat/EID";
3332
import { spawnNotePickup } from "../../entities/pickups/helper";
3433
import type { NoteInstance } from "../../entities/pickups/NotePickup/NotePickup";
3534
import {
@@ -41,6 +40,7 @@ import type { GlitchNoteTearData } from "../../entities/tears/GlitchNoteTear/Gli
4140
import { CollectibleTypeCustom } from "../../items/enum";
4241
import { mod } from "../../mod";
4342
import { getFireRateMultiplier, setFireRate } from "../../util/calc";
43+
import { updateCollectibleCostumes } from "../../util/costumes";
4444
import { getData } from "../../util/data";
4545
import { Debugger } from "../../util/debug";
4646
import { setTearColor } from "../../util/effects";
@@ -72,6 +72,11 @@ const ITEM_REPLACEMENTS: Partial<Record<CollectibleType, CollectibleType>> = {
7272
[CollectibleType.DR_FETUS]: CollectibleTypeCustom.DR_FETUS_NOTE,
7373
} as const;
7474

75+
const ITEM_COSTUMES: Partial<Record<CollectibleType, CollectibleType>> = {
76+
[CollectibleTypeCustom.BRIMSTONE_NOTE]: CollectibleType.BRIMSTONE,
77+
[CollectibleTypeCustom.DR_FETUS_NOTE]: CollectibleType.DR_FETUS,
78+
} as const;
79+
7580
const FIRE_RATE_ITEM_MULTIPLIERS: Partial<Record<CollectibleType, number>> = {
7681
[CollectibleType.MONSTROS_LUNG]: 0.35,
7782
} as const;
@@ -90,7 +95,7 @@ export class MikuTaintedCharacter extends Character {
9095
/** Font for the uses text of the notes. */
9196
private font: Font | undefined = undefined;
9297

93-
@CallbackCustom(ModCallbackCustom.POST_GAME_STARTED_REORDERED, true)
98+
@CallbackCustom(ModCallbackCustom.POST_GAME_STARTED_REORDERED, undefined)
9499
override onGameStart(isContinued: boolean): void {
95100
if (!isContinued || !mod.HasData()) {
96101
return;
@@ -166,6 +171,7 @@ export class MikuTaintedCharacter extends Character {
166171
*/
167172
@Callback(ModCallback.POST_PLAYER_INIT, PlayerVariant.PLAYER)
168173
override postPlayerInit(player: EntityPlayer): void {
174+
super.postPlayerInit(player);
169175
if (!isMiku(player, true)) {
170176
return;
171177
}
@@ -179,6 +185,8 @@ export class MikuTaintedCharacter extends Character {
179185
playerData.erased = saved.erased;
180186
playerData.notes = saved.notes;
181187
playerData.useNotes = saved.useNotes;
188+
189+
this.postPlayerInit(player);
182190
}
183191

184192
@CallbackCustom(
@@ -226,8 +234,7 @@ export class MikuTaintedCharacter extends Character {
226234
FIRE_RATE_ITEM_MULTIPLIERS,
227235
);
228236

229-
// If no item with extra multiplier was found, return to default item eval.
230-
if (multiplier === 1) {
237+
if (multiplier === -1) {
231238
return;
232239
}
233240

@@ -326,8 +333,11 @@ export class MikuTaintedCharacter extends Character {
326333
const isRightSide = controllerSides[index] ?? false;
327334

328335
// HUD layout config
329-
const startX = isRightSide ? 300 : 45;
330-
const startY = 45;
336+
const hudOffset = Options.HUDOffset;
337+
const hudX = hudOffset * 20;
338+
339+
const startX = isRightSide ? 300 - hudX : 45 + hudX;
340+
const startY = 60;
331341
const spacing = 16;
332342
const baseSize = 14;
333343
const maxPerRow = 5;
@@ -478,17 +488,7 @@ export class MikuTaintedCharacter extends Character {
478488
}
479489
}
480490

481-
if (player.HasCollectible(CollectibleTypeCustom.BRIMSTONE_NOTE)) {
482-
addCollectibleCostume(player, CollectibleType.BRIMSTONE);
483-
} else {
484-
removeCollectibleCostume(player, CollectibleType.BRIMSTONE);
485-
}
486-
487-
if (player.HasCollectible(CollectibleTypeCustom.DR_FETUS_NOTE)) {
488-
addCollectibleCostume(player, CollectibleType.DR_FETUS);
489-
} else {
490-
removeCollectibleCostume(player, CollectibleType.DR_FETUS);
491-
}
491+
updateCollectibleCostumes(player, ITEM_COSTUMES);
492492
}
493493
}
494494

@@ -585,6 +585,12 @@ export class MikuTaintedCharacter extends Character {
585585
const synergyNote = ITEM_SYNERGIES[itemID];
586586
const replaceItem = ITEM_REPLACEMENTS[itemID];
587587

588+
// FIXME: Solution until I find a better one...
589+
if (itemID === CollectibleType.LUDOVICO_TECHNIQUE) {
590+
player.AddCollectible(CollectibleType.TECH_5);
591+
return undefined;
592+
}
593+
588594
if (synergyNote === undefined || replaceItem === undefined) {
589595
return undefined;
590596
}
@@ -640,5 +646,22 @@ export class MikuTaintedCharacter extends Character {
640646
`${NAME} (Tainted)`,
641647
"Add description and birthright description.",
642648
);
649+
650+
// TODO: Implement with loop.
651+
appendToDescription(
652+
eid,
653+
"Brimstone",
654+
PlayerTypeCustom.MIKU_B,
655+
`#{{Player${PlayerTypeCustom.MIKU_B}}} Replaces {{Collectible118}} Brimstone#{{Collectible${CollectibleTypeCustom.BRIMSTONE_NOTE}}} Brimstone Notes can now drop from enemies`,
656+
() => anyPlayerIs(PlayerTypeCustom.MIKU_B),
657+
);
658+
659+
appendToDescription(
660+
eid,
661+
"Dr. Fetus",
662+
PlayerTypeCustom.MIKU_B,
663+
`#{{Player${PlayerTypeCustom.MIKU_B}}} Replaces {{Collectible52}} Dr. Fetus#{{Collectible${CollectibleTypeCustom.DR_FETUS_NOTE}}} Dr. Fetus Explosive Notes can now drop from enemies`,
664+
() => anyPlayerIs(PlayerTypeCustom.MIKU_B),
665+
);
643666
}
644667
}

src/compat/EID.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { PlayerType } from "isaac-typescript-definitions";
2+
13
export interface EIDExtended extends EIDInterface {
24
addCharacterInfo: (
35
characterId: int,
@@ -11,3 +13,31 @@ export interface EIDExtended extends EIDInterface {
1113
callback: (descObj: EIDDescriptionObject) => void,
1214
) => void;
1315
}
16+
17+
/**
18+
* Registers an **External Item Descriptions (EID)** description modifier that appends additional
19+
* text to an item description for a specific condition when a runtime condition is met.
20+
*
21+
* @param eid The `EIDExtended` instance used to register the description modifier.
22+
* @param name The name of the player used to generate a unique modifier identifier.
23+
* @param type The player type associated with the modifier.
24+
* @param description The text to append to Brimstone's EID description.
25+
* @param condition A callback that determines whether the description should be appended.
26+
* @see {@link EIDExtended}
27+
*/
28+
export const appendToDescription = (
29+
eid: EIDExtended,
30+
name: string,
31+
type: PlayerType,
32+
description: string,
33+
condition: () => boolean,
34+
): void => {
35+
eid.addDescriptionModifier(
36+
`${name}-${type}`,
37+
(descObj) => condition() && descObj.Name === name,
38+
(oldDescription) => {
39+
eid.appendToDescription(oldDescription, description);
40+
return oldDescription;
41+
},
42+
);
43+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { EIDExtended } from "../../compat/EID";
2+
import { Debugger } from "../../util/debug";
3+
import { Item } from "../Item";
4+
import { CollectibleTypeCustom } from "../enum";
5+
6+
const NAME = "Dr. Fetus Note";
7+
const DESCRIPTION = `Replaces {{Collectible52}} Dr. Fetus#{{Collectible${CollectibleTypeCustom.DR_FETUS_NOTE}}} Dr. Fetus Explosive Notes can now drop from enemies`;
8+
9+
export class DrFetusNote extends Item {
10+
override setupEID(eid: EIDExtended): void {
11+
eid.addCollectible(CollectibleTypeCustom.DR_FETUS_NOTE, DESCRIPTION);
12+
Debugger.eid(NAME, "Add description.");
13+
}
14+
}

src/main.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ import { GlitchNoteTear } from "./entities/tears/GlitchNoteTear/GlitchNoteTear";
1111
import { MusicalNoteTear } from "./entities/tears/MusicalNoteTear/MusicalNoteTear";
1212
import { BrimstoneNoteItem } from "./items/BrimstoneNoteItem/BrimstoneNoteItem";
1313
import { BrokenVoiceItem } from "./items/BrokenVoiceItem/BrokenVoiceItem";
14+
import { DrFetusNote } from "./items/DrFetusNote/DrFetusNote";
1415
import { EncoreItem } from "./items/EncoreItem/EncoreItem";
1516
import { MicrophoneItem } from "./items/MicrophoneItem/MicrophoneItem";
1617
import { VirtualIdolItem } from "./items/VirtualIdolItem/VirtualIdolItem";
1718
import { mod, MOD_NAME } from "./mod";
1819

19-
const PASSIVE_ITEMS = [VirtualIdolItem, EncoreItem, BrimstoneNoteItem] as const;
20+
const PASSIVE_ITEMS = [
21+
VirtualIdolItem,
22+
EncoreItem,
23+
BrimstoneNoteItem,
24+
DrFetusNote,
25+
] as const;
2026

2127
const ACTIVE_ITEMS = [MicrophoneItem, BrokenVoiceItem] as const;
2228

@@ -26,14 +32,16 @@ const PICKUPS = [NotePickup] as const;
2632

2733
const CHARACTERS = [MikuCharacter, MikuTaintedCharacter] as const;
2834

35+
const FEATURES = [
36+
...PASSIVE_ITEMS,
37+
...ACTIVE_ITEMS,
38+
...TEARS,
39+
...PICKUPS,
40+
...CHARACTERS,
41+
] as const;
42+
2943
export const main = (): void => {
30-
initModFeatures(mod, [
31-
...PASSIVE_ITEMS,
32-
...ACTIVE_ITEMS,
33-
...TEARS,
34-
...PICKUPS,
35-
...CHARACTERS,
36-
]);
44+
initModFeatures(mod, FEATURES);
3745

3846
NotePickup.register();
3947

src/util/costumes.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { CollectibleType } from "isaac-typescript-definitions";
2+
import {
3+
addCollectibleCostume,
4+
removeCollectibleCostume,
5+
} from "isaacscript-common";
6+
7+
/**
8+
* Updates collectible costumes based on a mapping of owned collectibles.
9+
* - Adds the corresponding costume when the player owns the mapped collectible.
10+
* - Removes the costume when the collectible is no longer owned.
11+
*
12+
* @param player The player whose collectible costumes should be updated.
13+
* @param costumes A mapping of collectible IDs to the costume collectible they should apply.
14+
*/
15+
export const updateCollectibleCostumes = (
16+
player: EntityPlayer,
17+
costumes: Partial<Record<CollectibleType, CollectibleType>>,
18+
): void => {
19+
for (const [item, costume] of Object.entries(costumes)) {
20+
const collectible = Number(item) as CollectibleType;
21+
const costumeCollectible = costume;
22+
23+
if (player.HasCollectible(collectible)) {
24+
addCollectibleCostume(player, costumeCollectible);
25+
} else {
26+
removeCollectibleCostume(player, costumeCollectible);
27+
}
28+
}
29+
};

src/util/debug.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const DEBUG = {
66
ITEMS: false,
77
TEAR: false,
88
PICKUP: false,
9-
EID: false,
9+
EID: true,
1010
} as const;
1111

1212
/** Debugging utility functions for the mod. */

0 commit comments

Comments
 (0)