Skip to content

Commit 73e2b54

Browse files
committed
Miku: add item blacklist for glitch notes mode
1 parent 06043f1 commit 73e2b54

4 files changed

Lines changed: 153 additions & 43 deletions

File tree

src/characters/Miku/MikuTaintedCharacter.ts

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
PlayerVariant,
1212
SoundEffect,
1313
} from "isaac-typescript-definitions";
14-
import type { SaveData } from "isaacscript-common";
1514
import {
1615
anyPlayerIs,
1716
Callback,
@@ -47,16 +46,31 @@ import { Debugger } from "../../util/debug";
4746
import { setTearColor } from "../../util/effects";
4847
import { eraseEnemies, getEnemyKey } from "../../util/enemies";
4948
import { rollWeighted } from "../../util/rng";
49+
import type { ModSaveData } from "../../util/save";
5050
import { SAVE_DATA } from "../../util/save";
5151
import { Character } from "../Character";
5252
import { isMiku, PlayerTypeCustom } from "../enum";
5353
import type { MikuPlayerData } from "./MikuCharacter";
54+
import {
55+
isNoteItemDisabled,
56+
MikuAttackMode as MikuNoteMode,
57+
setMikuAttackMode,
58+
} from "./mikuHelper";
5459

5560
export interface TaintedMikuData extends MikuPlayerData {
56-
useNotes?: boolean;
61+
attackMode?: MikuNoteMode;
62+
notes?: NoteInstance[];
63+
unlockedNotes?: NotePickupSubType[];
64+
erased?: string[];
65+
storedCollectibles?: Set<CollectibleType>;
66+
}
67+
68+
export interface TaintedMikuSaveData {
69+
attackMode?: MikuNoteMode;
5770
notes?: NoteInstance[];
5871
unlockedNotes?: NotePickupSubType[];
5972
erased?: string[];
73+
storedCollectibles?: CollectibleType[];
6074
}
6175

6276
const NAME = "Miku";
@@ -65,7 +79,7 @@ const BIRTHRIGHT_DESC = "TODO";
6579
const HAIR = Isaac.GetCostumeIdByPath("gfx/characters/Character_MikuHead.anm2");
6680
const POCKET_ACTIVE = CollectibleTypeCustom.BROKEN_VOICE;
6781
const NULL_ITEM = CollectibleTypeCustom.MIKU_IDOL;
68-
const NOTE_DROP_CHANCE = 65;
82+
const NOTE_DROP_CHANCE = 45;
6983

7084
const ITEM_REPLACEMENTS: Partial<Record<CollectibleType, CollectibleType>> = {
7185
[CollectibleType.BRIMSTONE]: CollectibleTypeCustom.BRIMSTONE_NOTE,
@@ -92,31 +106,35 @@ export class MikuTaintedCharacter extends Character {
92106
/** Font for the uses text of the notes. */
93107
private font: Font | undefined = undefined;
94108

95-
@CallbackCustom(ModCallbackCustom.POST_GAME_STARTED_REORDERED, undefined)
109+
@CallbackCustom(ModCallbackCustom.POST_GAME_STARTED_REORDERED, true)
96110
override onGameStart(isContinued: boolean): void {
97111
if (!isContinued || !mod.HasData()) {
98112
return;
99113
}
100114

101-
const loaded = jsonDecode(mod.LoadData()) as SaveData;
115+
const loaded = jsonDecode(mod.LoadData()) as unknown as ModSaveData;
102116

103117
Object.assign(SAVE_DATA, loaded);
104118
}
105119

106120
@Callback(ModCallback.PRE_GAME_EXIT)
107121
override onGameExit(): void {
108-
SAVE_DATA.players = {};
122+
SAVE_DATA.mikuBs = {};
109123

110124
const players = getPlayersOfType(PlayerTypeCustom.MIKU_B);
111125
for (const player of players) {
112126
const playerData = getData<TaintedMikuData>(player);
113127

114-
const { erased, notes, useNotes } = playerData;
128+
const { erased, notes, attackMode, storedCollectibles } = playerData;
115129

116-
SAVE_DATA.players[player.ControllerIndex.toString()] = {
130+
SAVE_DATA.mikuBs[player.ControllerIndex.toString()] = {
131+
attackMode: attackMode ?? MikuNoteMode.GLITCH,
117132
erased: erased ? [...erased] : [],
118133
notes: notes ? [...notes] : [],
119-
useNotes: useNotes ?? false,
134+
unlockedNotes: playerData.unlockedNotes
135+
? [...playerData.unlockedNotes]
136+
: [],
137+
storedCollectibles: storedCollectibles ? [...storedCollectibles] : [],
120138
};
121139
}
122140

@@ -138,10 +156,11 @@ export class MikuTaintedCharacter extends Character {
138156
override postPlayerInitFirst(player: EntityPlayer): void {
139157
const playerData = getData<TaintedMikuData>(player);
140158
playerData.hasIdol = false;
141-
playerData.useNotes = false;
159+
playerData.attackMode = MikuNoteMode.GLITCH;
142160
playerData.notes = [];
143161
playerData.erased = [];
144162
playerData.unlockedNotes = [];
163+
playerData.storedCollectibles = new Set();
145164

146165
player.AddNullCostume(HAIR);
147166
Debugger.char(`${NAME} (Tainted)`, `Applied null costume: ${HAIR}`);
@@ -170,17 +189,25 @@ export class MikuTaintedCharacter extends Character {
170189
return;
171190
}
172191

173-
const saved = SAVE_DATA.players[player.ControllerIndex.toString()];
192+
const saved = SAVE_DATA.mikuBs[player.ControllerIndex.toString()];
174193
if (!saved) {
175194
return;
176195
}
177196

178197
const playerData = getData<TaintedMikuData>(player);
179-
playerData.erased = saved.erased;
180-
playerData.notes = saved.notes;
181-
playerData.useNotes = saved.useNotes;
182198

183-
this.postPlayerInit(player);
199+
playerData.erased = saved.erased ?? [];
200+
playerData.notes = saved.notes ?? [];
201+
playerData.unlockedNotes = saved.unlockedNotes ?? [];
202+
playerData.attackMode = saved.attackMode ?? MikuNoteMode.GLITCH;
203+
204+
playerData.storedCollectibles = new Set(saved.storedCollectibles ?? []);
205+
206+
for (const collectible of playerData.storedCollectibles) {
207+
if (!player.HasCollectible(collectible)) {
208+
player.AddCollectible(collectible);
209+
}
210+
}
184211
}
185212

186213
@CallbackCustom(
@@ -190,7 +217,7 @@ export class MikuTaintedCharacter extends Character {
190217
)
191218
override postPlayerUpdate(player: EntityPlayer): void {
192219
const playerData = getData<TaintedMikuData>(player);
193-
const { notes, useNotes } = playerData;
220+
const { notes } = playerData;
194221

195222
if (!notes || notes.length === 0) {
196223
return;
@@ -201,7 +228,11 @@ export class MikuTaintedCharacter extends Character {
201228
player.ControllerIndex,
202229
);
203230

204-
if (!(useNotes ?? false) && isDropping && notes.length > 1) {
231+
if (
232+
playerData.attackMode === MikuNoteMode.VOICES
233+
&& isDropping
234+
&& notes.length > 1
235+
) {
205236
const firstNote = notes.shift();
206237
if (firstNote) {
207238
notes.push(firstNote);
@@ -251,9 +282,13 @@ export class MikuTaintedCharacter extends Character {
251282
}
252283

253284
const playerData = getData<TaintedMikuData>(player);
254-
const { notes, useNotes } = playerData;
285+
const { notes, attackMode } = playerData;
286+
287+
if (attackMode === MikuNoteMode.GLITCH) {
288+
return;
289+
}
255290

256-
if ((useNotes ?? false) || !notes || notes.length === 0) {
291+
if (!notes || notes.length === 0) {
257292
return;
258293
}
259294

@@ -311,7 +346,7 @@ export class MikuTaintedCharacter extends Character {
311346

312347
for (const player of players) {
313348
const playerData = getData<TaintedMikuData>(player);
314-
const { notes, useNotes } = playerData;
349+
const { notes, attackMode } = playerData;
315350
if (!notes || notes.length === 0) {
316351
continue;
317352
}
@@ -380,7 +415,7 @@ export class MikuTaintedCharacter extends Character {
380415
: noteConfig.color;
381416

382417
this.noteSprite.Color =
383-
(useNotes ?? false)
418+
attackMode === MikuNoteMode.GLITCH
384419
? Color(
385420
baseColor.R * 0.35,
386421
baseColor.G * 0.35,
@@ -404,7 +439,11 @@ export class MikuTaintedCharacter extends Character {
404439
// Render 'selected' note above player.
405440
const activeNote = notes[0];
406441

407-
if (!(useNotes ?? false) && activeNote && !isPausedCutscene) {
442+
if (
443+
attackMode === MikuNoteMode.VOICES
444+
&& activeNote
445+
&& !isPausedCutscene
446+
) {
408447
const noteConfig = NOTE_TYPE_DATA[activeNote.subType];
409448
const screenPos: Vector = Isaac.WorldToScreen(player.Position);
410449

@@ -569,21 +608,18 @@ export class MikuTaintedCharacter extends Character {
569608
return undefined;
570609
}
571610

611+
const data = getData<TaintedMikuData>(player);
612+
572613
const itemID = pickup.SubType as CollectibleType;
573614
const synergyNote = ITEM_SYNERGIES[itemID];
574615
const replaceItem = ITEM_REPLACEMENTS[itemID];
575616

576-
// FIXME: Solution until I find a better one...
577-
if (itemID === CollectibleType.LUDOVICO_TECHNIQUE) {
578-
player.AddCollectible(CollectibleType.TECH_5);
579-
return undefined;
580-
}
617+
this.checkDisabledItem(player, itemID);
581618

582619
if (synergyNote === undefined || replaceItem === undefined) {
583620
return undefined;
584621
}
585622

586-
const data = getData<TaintedMikuData>(player);
587623
data.unlockedNotes ??= [];
588624

589625
if (!data.unlockedNotes.includes(synergyNote)) {
@@ -644,6 +680,14 @@ export class MikuTaintedCharacter extends Character {
644680
() => anyPlayerIs(PlayerTypeCustom.MIKU_B),
645681
);
646682

683+
appendToDescription(
684+
eid,
685+
"Mom's Knife",
686+
PlayerTypeCustom.MIKU_B,
687+
`#{{Player${PlayerTypeCustom.MIKU_B}}} {{Warning}} Tainted Miku can use the knife only in her glitched mode`,
688+
() => anyPlayerIs(PlayerTypeCustom.MIKU_B),
689+
);
690+
647691
appendToDescription(
648692
eid,
649693
"Dr. Fetus",
@@ -652,4 +696,20 @@ export class MikuTaintedCharacter extends Character {
652696
() => anyPlayerIs(PlayerTypeCustom.MIKU_B),
653697
);
654698
}
699+
700+
private checkDisabledItem(
701+
player: EntityPlayer,
702+
itemID: CollectibleType,
703+
): void {
704+
const data = getData<TaintedMikuData>(player);
705+
706+
if (isNoteItemDisabled(itemID) && data.attackMode !== MikuNoteMode.GLITCH) {
707+
setMikuAttackMode(player, MikuNoteMode.GLITCH);
708+
709+
Debugger.char(
710+
NAME,
711+
`Switched to Glitch mode because ${itemID} is disabled in Notes mode.`,
712+
);
713+
}
714+
}
655715
}

src/characters/Miku/mikuHelper.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { CollectibleType } from "isaac-typescript-definitions";
2+
import { getData } from "../../util/data";
3+
import type { TaintedMikuData } from "./MikuTaintedCharacter";
4+
5+
export enum MikuAttackMode {
6+
GLITCH,
7+
VOICES,
8+
}
9+
10+
// eslint-disable-next-line complete/require-capital-read-only
11+
const NOTE_DISABLED_ITEMS: CollectibleType[] = [
12+
CollectibleType.MOMS_KNIFE,
13+
CollectibleType.SPIRIT_SWORD,
14+
CollectibleType.LUDOVICO_TECHNIQUE,
15+
] as const;
16+
17+
export const setMikuAttackMode = (
18+
player: EntityPlayer,
19+
mode: MikuAttackMode,
20+
): void => {
21+
const playerData = getData<TaintedMikuData>(player);
22+
23+
if (playerData.attackMode === mode) {
24+
return;
25+
}
26+
27+
playerData.storedCollectibles ??= new Set();
28+
29+
if (mode === MikuAttackMode.VOICES) {
30+
for (const item of NOTE_DISABLED_ITEMS) {
31+
if (player.HasCollectible(item)) {
32+
playerData.storedCollectibles.add(item);
33+
player.RemoveCollectible(item);
34+
}
35+
}
36+
} else {
37+
for (const item of playerData.storedCollectibles) {
38+
player.AddCollectible(item);
39+
}
40+
41+
playerData.storedCollectibles = new Set();
42+
}
43+
44+
playerData.attackMode = mode;
45+
};
46+
47+
export const isNoteItemDisabled = (collectible: CollectibleType): boolean =>
48+
NOTE_DISABLED_ITEMS.includes(collectible);

src/items/Miku/MikuB/BrokenVoiceItem/BrokenVoiceItem.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ import type {
33
CollectibleType,
44
UseFlag,
55
} from "isaac-typescript-definitions";
6-
import { ModCallback, SoundEffect } from "isaac-typescript-definitions";
6+
import { ModCallback } from "isaac-typescript-definitions";
77
import { Callback } from "isaacscript-common";
88
import { isMiku } from "../../../../characters/enum";
9+
import {
10+
MikuAttackMode,
11+
setMikuAttackMode,
12+
} from "../../../../characters/Miku/mikuHelper";
913
import type { TaintedMikuData } from "../../../../characters/Miku/MikuTaintedCharacter";
1014
import type { EIDExtended } from "../../../../compat/EID";
1115
import { getData } from "../../../../util/data";
@@ -40,16 +44,13 @@ export class BrokenVoiceItem extends ActiveItem {
4044
): UseItemResult {
4145
if (isMiku(player, true)) {
4246
const playerData = getData<TaintedMikuData>(player);
43-
if (playerData.notes && playerData.notes.length > 0) {
44-
playerData.useNotes = !(playerData.useNotes ?? false);
45-
SFXManager().Play(
46-
SoundEffect.STATIC,
47-
1,
48-
2,
49-
false,
50-
playerData.useNotes ? 1 : 1.75,
51-
);
52-
}
47+
48+
setMikuAttackMode(
49+
player,
50+
playerData.attackMode === MikuAttackMode.GLITCH
51+
? MikuAttackMode.VOICES
52+
: MikuAttackMode.GLITCH,
53+
);
5354
}
5455

5556
return {

src/util/save.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import type { TaintedMikuData } from "../characters/Miku/MikuTaintedCharacter";
1+
import type { SaveData } from "isaacscript-common";
2+
import type { TaintedMikuSaveData } from "../characters/Miku/MikuTaintedCharacter";
23

3-
interface ModSaveData {
4-
players: Record<string, TaintedMikuData>;
4+
export interface ModSaveData extends SaveData {
5+
mikuBs: Record<string, TaintedMikuSaveData>;
56
}
67

78
export const SAVE_DATA: ModSaveData = {
8-
players: {},
9+
mikuBs: {},
910
} as const;

0 commit comments

Comments
 (0)