Skip to content

Commit 2e17468

Browse files
committed
Miku: implement Birthright multi-note firing mechanic
1 parent 73e2b54 commit 2e17468

7 files changed

Lines changed: 153 additions & 69 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ Use her **Microphone** to inspire your audience!
2222
Due to losing her voice, she starts with only Glitch Notes.
2323

2424
Enemies may drop **Lost Notes** when slain.
25-
Each **Note** grants a **different effect**.
25+
Each **Lost Note** grants a **different effect**.
2626

2727
Use the **Broken Microphone** to switch melodies.
2828

29+
With **Birthright** she loses even more control of her broken voice, firing multiple Lost Notes at onces.
30+
2931
## Mod Compatibility
3032

3133
### External Item Descriptions Support
@@ -37,11 +39,10 @@ For missing or wrong descriptions, please open a new Issue.
3739
## Roadmap
3840

3941
- Finish Tainted Miku.
40-
- Add Tainted Miku Birthright effect.
42+
- Sound Effects.
4143
- More Miku-themed items.
4244
- Improved sprites and animations.
4345
- Better visual effects.
44-
- Expanded mechanics and balance improvements.
4546

4647
## Disclaimer
4748

TODO.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [x] Add 'Passive Active Item' to switch between notes.
1010
- [x] Add Synergies to Tainted Miku. (e.g. Brimstone, Epic Fetus)
1111
- [ ] Add MCM Compat.
12+
- [ ] Setup Tests.
1213

1314
## TODO: Miku
1415

@@ -31,5 +32,5 @@
3132
- [ ] TODO: Synergy ideas.
3233
- [x] Fix fire rate multiplier issues with several items.
3334
- [x] Fix Monstro's Lung.
34-
- [ ] Add Birthright for Tainted Miku.
35+
- [x] Add Birthright for Tainted Miku.
3536
- [ ] Add Tainted Miku Icons & Controls.

mod/content/players.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<players root="gfx/characters/costumes/" portraitroot="gfx/ui/stage/" nameimageroot="gfx/ui/boss/">
22
<player name="Miku" skin="Character_Miku.png" nameimage="PlayerName_Miku.png" hp="6" portrait="PlayerPortrait_Miku.png" skinColor="-1" costume="104" costumeSuffix="miku" birthright="Lucky idol" />
3-
<player name="Miku" skin="Character_Miku_b.png" nameimage="PlayerName_Miku.png" hp="4" armor="2" keys="1" portrait="PlayerPortrait_Miku.png" skinColor="-1" bSkinParent="Miku" costume="104" costumeSuffix="mikub" />
3+
<player name="Miku" skin="Character_Miku_b.png" nameimage="PlayerName_Miku.png" hp="4" armor="2" keys="1" portrait="PlayerPortrait_Miku.png" skinColor="-1" bSkinParent="Miku" costume="104" costumeSuffix="mikub" birthright="Broken Encore" />
44
</players>

mod/metadata.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ I get a lot of comments like the games not launching or the mod breaks the game.
2525
• Use her active item, the [b]Microphone[/b], to inspire your audience!
2626

2727
[b]Tainted Miku (Work In Progress)[/b]
28+
• [NEW] Unique birthright effect.
2829
• Having lost her voice, she starts the run firing highly unstable [b]Glitch Notes[/b].
2930
• Slain enemies drop [b]Lost Notes[/b] onto the floor.
3031
• Collecting different Lost Notes grants various dynamic gameplay effects.
31-
• Use the [b]Broken Microphone[/b] to rapidly switch melodies.
32+
• Use the [b]Broken Microphone[/b] to switch between melodies.
3233

3334
----------------------------------------------------------------------
3435
[h2]Mod Compatibility[/h2]
@@ -58,6 +59,6 @@ This is a non-commercial, fan-made mod created as a personal project for fun. Al
5859
• [url=https://steamcommunity.com/profiles/76561198102520074]ale[/url] — For creating the amazing Miku/Vocaloid sprites!
5960
• [url=https://steamcommunity.com/id/thearexz]Arexz[/url] — For helping with custom sprite editing and refinement.
6061
</description>
61-
<version>0.3.1</version>
62+
<version>0.4.0</version>
6263
<visibility />
6364
</metadata>

src/characters/Miku/MikuTaintedCharacter.ts

Lines changed: 84 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
ModCallbackCustom,
2525
ReadonlyMap,
2626
spawnEffect,
27+
spawnTear,
2728
VectorZero,
2829
} from "isaacscript-common";
2930
import type { EIDExtended } from "../../compat/EID";
@@ -75,7 +76,9 @@ export interface TaintedMikuSaveData {
7576

7677
const NAME = "Miku";
7778
const DESCRIPTION = "A twisted idol, using enemies as her melody.";
78-
const BIRTHRIGHT_DESC = "TODO";
79+
const BIRTHRIGHT_DESC =
80+
"Her fractured melody splits apart, causing multiple Lost Notes to fire with every shot.";
81+
const BIRTHRIGHT_MULTI_SHOT = 5;
7982
const HAIR = Isaac.GetCostumeIdByPath("gfx/characters/Character_MikuHead.anm2");
8083
const POCKET_ACTIVE = CollectibleTypeCustom.BROKEN_VOICE;
8184
const NULL_ITEM = CollectibleTypeCustom.MIKU_IDOL;
@@ -175,41 +178,6 @@ export class MikuTaintedCharacter extends Character {
175178
}
176179
}
177180

178-
/**
179-
* Called after Tainted Miku is initialized.
180-
*
181-
* Reads the mod save data to set the data for Tainted Miku on a continued run.
182-
*
183-
* @param player The player entity being initialized.
184-
*/
185-
@Callback(ModCallback.POST_PLAYER_INIT, PlayerVariant.PLAYER)
186-
override postPlayerInit(player: EntityPlayer): void {
187-
super.postPlayerInit(player);
188-
if (!isMiku(player, true)) {
189-
return;
190-
}
191-
192-
const saved = SAVE_DATA.mikuBs[player.ControllerIndex.toString()];
193-
if (!saved) {
194-
return;
195-
}
196-
197-
const playerData = getData<TaintedMikuData>(player);
198-
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-
}
211-
}
212-
213181
@CallbackCustom(
214182
ModCallbackCustom.POST_PLAYER_UPDATE_REORDERED,
215183
PlayerVariant.PLAYER,
@@ -248,6 +216,41 @@ export class MikuTaintedCharacter extends Character {
248216
}
249217
}
250218

219+
/**
220+
* Called after Tainted Miku is initialized.
221+
*
222+
* Reads the mod save data to set the data for Tainted Miku on a continued run.
223+
*
224+
* @param player The player entity being initialized.
225+
*/
226+
@Callback(ModCallback.POST_PLAYER_INIT, PlayerVariant.PLAYER)
227+
override postPlayerInit(player: EntityPlayer): void {
228+
super.postPlayerInit(player);
229+
if (!isMiku(player, true)) {
230+
return;
231+
}
232+
233+
const saved = SAVE_DATA.mikuBs[player.ControllerIndex.toString()];
234+
if (!saved) {
235+
return;
236+
}
237+
238+
const playerData = getData<TaintedMikuData>(player);
239+
240+
playerData.erased = saved.erased ?? [];
241+
playerData.notes = saved.notes ?? [];
242+
playerData.unlockedNotes = saved.unlockedNotes ?? [];
243+
playerData.attackMode = saved.attackMode ?? MikuNoteMode.GLITCH;
244+
245+
playerData.storedCollectibles = new Set(saved.storedCollectibles ?? []);
246+
247+
for (const collectible of playerData.storedCollectibles) {
248+
if (!player.HasCollectible(collectible)) {
249+
player.AddCollectible(collectible);
250+
}
251+
}
252+
}
253+
251254
@Callback(ModCallback.EVALUATE_CACHE, CacheFlag.FIRE_DELAY)
252255
override cacheFireDelay(player: EntityPlayer): void {
253256
if (!isMiku(player, true)) {
@@ -277,6 +280,7 @@ export class MikuTaintedCharacter extends Character {
277280
@Callback(ModCallback.POST_FIRE_TEAR)
278281
override postFireTear(tear: EntityTear): void {
279282
const player = getPlayerFromEntity(tear);
283+
280284
if (!player || !isMiku(player, true)) {
281285
return;
282286
}
@@ -292,21 +296,32 @@ export class MikuTaintedCharacter extends Character {
292296
return;
293297
}
294298

295-
const note = notes[0];
296-
if (!note) {
297-
return;
298-
}
299+
const amount = player.HasCollectible(CollectibleType.BIRTHRIGHT)
300+
? BIRTHRIGHT_MULTI_SHOT
301+
: 1;
299302

300-
const noteData = NOTE_TYPE_DATA[note.subType];
301-
const tearData = getData<GlitchNoteTearData>(tear);
303+
const activeNotes = notes.slice(0, amount);
302304

303-
noteData.applyEffect?.(player, tear);
304-
noteData.onFireTear?.(player, tear);
305-
tearData.color = noteData.color;
305+
for (const [i, note] of activeNotes.entries()) {
306+
let noteTear = tear;
306307

307-
note.remainingUses--;
308-
if (note.remainingUses <= 0) {
309-
notes.shift();
308+
if (i > 0) {
309+
noteTear = spawnTear(
310+
tear.Variant,
311+
tear.SubType,
312+
tear.Position,
313+
tear.Velocity,
314+
player,
315+
);
316+
317+
noteTear.CollisionDamage = tear.CollisionDamage;
318+
noteTear.Scale = tear.Scale;
319+
}
320+
321+
this.applyNoteEffect(player, noteTear, note);
322+
323+
const tearData = getData<GlitchNoteTearData>(noteTear);
324+
setTearColor(noteTear, tearData, noteTear.GetDropRNG());
310325
}
311326
}
312327

@@ -712,4 +727,25 @@ export class MikuTaintedCharacter extends Character {
712727
);
713728
}
714729
}
730+
731+
private applyNoteEffect(
732+
player: EntityPlayer,
733+
tear: EntityTear,
734+
note: NoteInstance,
735+
): void {
736+
const noteData = NOTE_TYPE_DATA[note.subType];
737+
const tearData = getData<GlitchNoteTearData>(tear);
738+
739+
noteData.applyEffect?.(player, tear);
740+
noteData.onFireTear?.(player, tear);
741+
742+
tearData.color = noteData.color;
743+
744+
note.remainingUses--;
745+
746+
if (note.remainingUses <= 0) {
747+
const playerData = getData<TaintedMikuData>(player);
748+
playerData.notes = playerData.notes?.filter((n) => n !== note);
749+
}
750+
}
715751
}

src/entities/tears/GlitchNoteTear/GlitchNoteTear.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { Tear } from "../Tear";
1919

2020
export interface GlitchNoteTearData extends TearData {
2121
onHitEnemy?: (entity: EntityNPC) => void;
22+
glitchTime?: number;
23+
initializedColor?: boolean;
2224
}
2325

2426
export class GlitchNoteTear extends Tear {
@@ -46,13 +48,42 @@ export class GlitchNoteTear extends Tear {
4648
spawnPoof(tear, EffectVariant.TEAR_POOF_B);
4749
}
4850

49-
private applyEffect(tear: EntityTear) {
51+
private applyEffect(tear: EntityTear): void {
5052
const tearData = getData<GlitchNoteTearData>(tear);
53+
54+
tearData.glitchTime ??= 0;
55+
tearData.glitchTime++;
56+
5157
const rng = tear.GetDropRNG();
5258

53-
setTearColor(tear, tearData, rng);
59+
if (!(tearData.initializedColor ?? false)) {
60+
setTearColor(tear, tearData, rng);
61+
tearData.initializedColor = true;
62+
}
63+
64+
const maxTime = 20;
65+
const intensity = Math.min(tearData.glitchTime / maxTime, 1);
66+
67+
const minBrightness = 0.15;
68+
const brightness = minBrightness + (1 - minBrightness) * (1 - intensity);
69+
70+
const originalColor = tearData.color;
71+
72+
tear.SetColor(
73+
Color(
74+
brightness * (originalColor?.R ?? 1),
75+
brightness * (originalColor?.G ?? 1),
76+
brightness * (originalColor?.B ?? 1),
77+
1,
78+
0,
79+
0,
80+
0,
81+
),
82+
-1,
83+
1000,
84+
);
5485

55-
applyPositionJitter(tear, rng);
56-
applyRotationShift(tear, rng);
86+
applyPositionJitter(tear, rng, intensity);
87+
applyRotationShift(tear, rng, intensity);
5788
}
5889
}

src/util/effects.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,47 @@ export const setTearColor = (
2727
/**
2828
* Applies a subtle "floaty" jitter effect to the tear's position.
2929
*
30-
* - This function randomly offsets the tear's position slightly, creating an organic and jittery
31-
* effect in the tear's movement. The intensity of the jitter is determined by a random chance and
32-
* the RNG value.
30+
* - This function randomly offsets the tear's position slightly, creating an kinda organic and
31+
* jittery effect in the tear's movement.
32+
* - The intensity increases the strength of the jitter.
3333
*
3434
* @param tear The tear entity whose position will be modified by the jitter effect.
3535
* @param rng A random number generator instance used to determine the jitter amount.
36+
* @param intensity Strength multiplier for the effect (0-1).
3637
*/
37-
export const applyPositionJitter = (tear: EntityTear, rng: RNG): void => {
38+
export const applyPositionJitter = (
39+
tear: EntityTear,
40+
rng: RNG,
41+
intensity = 1,
42+
): void => {
3843
if (rollChance(15, rng, 0)) {
39-
const jitterX = (getRandom(rng) - 0.5) * 0.3;
40-
const jitterY = (getRandom(rng) - 0.5) * 0.3;
44+
const amount = 0.3 * intensity;
45+
46+
const jitterX = (getRandom(rng) - 0.5) * amount;
47+
const jitterY = (getRandom(rng) - 0.5) * amount;
48+
4149
tear.Position = tear.Position.add(Vector(jitterX, jitterY));
4250
}
4351
};
4452

4553
/**
4654
* Applies random rotation shifts to the tear, creating a less rigid, more dynamic motion.
4755
*
48-
* - This function adds a small random shift to the tear's rotation. The shift amount is determined
49-
* by the RNG.
56+
* - The rotation distortion increases with the intensity value.
5057
*
5158
* @param tear The tear entity whose rotation will be modified by the rotation shift.
5259
* @param rng A random number generator instance used to determine the rotation shift amount.
60+
* @param intensity Strength multiplier for the effect (0-1).
5361
*/
54-
export const applyRotationShift = (tear: EntityTear, rng: RNG): void => {
62+
export const applyRotationShift = (
63+
tear: EntityTear,
64+
rng: RNG,
65+
intensity = 1,
66+
): void => {
5567
if (rollChance(20, rng, 0)) {
56-
const rotationShift = (getRandom(rng) - 0.5) * 4;
68+
const amount = 4 * intensity;
69+
70+
const rotationShift = (getRandom(rng) - 0.5) * amount;
5771
tear.SpriteRotation += rotationShift;
5872
}
5973
};

0 commit comments

Comments
 (0)