From 884fa86aabfd62d3c800b09349b3b6bdb1714103 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 22 Aug 2025 18:29:14 -0400 Subject: [PATCH 001/128] Add UI option for z2mario --- CrossPlatformUI/Lang/Resources.resx | 5 +++++ CrossPlatformUI/Views/Tabs/SpellsView.axaml | 6 ++++++ RandomizerCore/Hyrule.cs | 5 +++-- RandomizerCore/RandomizerConfiguration.cs | 5 +++++ RandomizerCore/RandomizerProperties.cs | 1 + 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CrossPlatformUI/Lang/Resources.resx b/CrossPlatformUI/Lang/Resources.resx index a336781cc..b5b193dff 100644 --- a/CrossPlatformUI/Lang/Resources.resx +++ b/CrossPlatformUI/Lang/Resources.resx @@ -956,4 +956,9 @@ Use this only when everyone intentionally wants the same base seed with differen For flags set to an indeterminate value (question mark), those flags will be on or off at random, at the freqency specified by this flag. + + Play as Mario from SMB1 using 100% authentic Mario physics and controls! + +This is a wildly different gameplay experience, so be warned when setting this flag. + diff --git a/CrossPlatformUI/Views/Tabs/SpellsView.axaml b/CrossPlatformUI/Views/Tabs/SpellsView.axaml index 2abd5d472..d39fb3574 100644 --- a/CrossPlatformUI/Views/Tabs/SpellsView.axaml +++ b/CrossPlatformUI/Views/Tabs/SpellsView.axaml @@ -74,6 +74,12 @@ > + + + diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index f265edda7..1a893207a 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3778,11 +3778,12 @@ .word NewCreditsBody """, "add_credits.s"); } - private void ChangeMapperToMMC5(Assembler asm, bool preventFlash, bool enableZ2Ft) + private void ChangeMapperToMMC5(Assembler asm, bool preventFlash, bool enableZ2Ft, bool enableZ2Mario) { var a = asm.Module(); a.Assign("PREVENT_HUD_FLASH_ON_LAG", preventFlash ? 1 : 0); a.Assign("ENABLE_Z2FT", enableZ2Ft ? 1 : 0); + a.Assign("ENABLE_Z2_MARIO", enableZ2Mario ? 1 : 0); AssignRealPalaceLocations(a); a.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.MMC5.s"), "mmc5_conversion.s"); } @@ -3791,7 +3792,7 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando { bool randomizeMusic = !props.DisableMusic && props.RandomizeMusic; - ChangeMapperToMMC5(engine, props.DisableHUDLag, randomizeMusic); // will make output vary with customize tab options + ChangeMapperToMMC5(engine, props.DisableHUDLag, randomizeMusic, props.MarioMode); // will make output vary with customize tab options rom.AddRandomizerToTitle(engine); AddCropGuideBoxesToFileSelect(engine); FixHelmetheadBossRoom(engine); diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 9853cc6f5..eafb1c1f8 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -681,6 +681,9 @@ private bool palaceStylesAnyMetastyleSelected() [Reactive] private bool permanentBeamSword; + [Reactive] + private bool marioMode; + //Custom [Reactive] [IgnoreInFlags] @@ -840,6 +843,7 @@ public RandomizerConfiguration() fastSpellCasting = false; shuffleSpritePalettes = false; permanentBeamSword = false; + marioMode = false; upAOnController1 = false; removeFlashing = true; sprite = CharacterSprite.LINK; @@ -1065,6 +1069,7 @@ public RandomizerProperties Export(Random r, bool includeDifficulty = true) properties.StartLives = ResolveStartingLives(r, includeDifficulty); properties.PermanentBeam = permanentBeamSword; + properties.MarioMode = marioMode; properties.UseCommunityText = useCommunityText; // If shared difficulty is enabled and we're setting up the shared diff --git a/RandomizerCore/RandomizerProperties.cs b/RandomizerCore/RandomizerProperties.cs index 271d0d85d..ab8d747e0 100644 --- a/RandomizerCore/RandomizerProperties.cs +++ b/RandomizerCore/RandomizerProperties.cs @@ -214,6 +214,7 @@ public class RandomizerProperties public bool TownNameHints { get; set; } //Misc. + public bool MarioMode { get; set; } public byte BeepFrequency { get; set; } public byte BeepThreshold { get; set; } public bool JumpAlwaysOn { get; set; } From 514c900d2342a2d9ccb78d98af5e799bd08db64d Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 22 Aug 2025 18:31:40 -0400 Subject: [PATCH 002/128] Initial port of the beta z2mario code --- RandomizerCore/Asm/z2mario/integration.s | 526 +++++++++++++ RandomizerCore/Asm/z2mario/map.s | 167 ++++ RandomizerCore/Asm/z2mario/map_mario.chr | Bin 0 -> 2048 bytes RandomizerCore/Asm/z2mario/metasprite.s | 604 +++++++++++++++ .../Asm/z2mario/metasprite_engine.s | 722 ++++++++++++++++++ RandomizerCore/Asm/z2mario/sfx.s | 600 +++++++++++++++ RandomizerCore/Asm/z2r.inc | 295 +++++++ 7 files changed, 2914 insertions(+) create mode 100644 RandomizerCore/Asm/z2mario/integration.s create mode 100644 RandomizerCore/Asm/z2mario/map.s create mode 100644 RandomizerCore/Asm/z2mario/map_mario.chr create mode 100644 RandomizerCore/Asm/z2mario/metasprite.s create mode 100644 RandomizerCore/Asm/z2mario/metasprite_engine.s create mode 100644 RandomizerCore/Asm/z2mario/sfx.s diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s new file mode 100644 index 000000000..e351fc42b --- /dev/null +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -0,0 +1,526 @@ + +; Remove unused code after we gutted link's movement +FREE "PRG0" [$92BF, $962D) + +; .segment "PRG0" +; .org (($10ea - $10) .mod $4000) + $8000 +; .byte $17 + + +.segment "PRG0", "PRG7" +.org $D3EC ; patches the main sideview routine right before checking marios code + jsr SwapMarioCHRBanks +.reloc +SwapMarioCHRBanks: + jsr BankSwitchMarioCHR + jmp $8DC3 + +.reloc +BankSwitchMarioCHR: + lda ReloadCHRBank + beq + +; .repeat 8, I +; lda CurrentCHRBank + I +; sta MMC5_CHR_BANK_BASE + I +; .endrepeat + ; brk + ; nop + lda PlayerChrBank + sta MMC5_CHR_BANK_BASE + 0 + lda CurrentCHRBank ; we need to write a BG bank register due to MMC5 jank + sta MMC5_CHR_BANK_BASE + 8 + + + rts + +; patch the link draw routine to skip drawing him with the vanilla code +.org $EC02 ; ldx $11 lda $29,x + jsr PatchLinkDrawRoutine + nop + +.reloc +PatchLinkDrawRoutine: + ldx $11 + beq + ; link is slot 0 + lda $29,x + rts ++ + ; for now assume its always PRG0 (it seems to be at least in my limited testing) + ; jsr SwapToPRG0 + + ; put the player in slot 4 always + lda #4 + sta CurrentOAMOffset + + ; draw the player first so it doesn't ever flicker + ; first clear out the sprites that are reserved for the player + ; lda #$f8 + ; sta Sprite_Y_Position + 4 + ; sta Sprite_Y_Position + 8 + ; sta Sprite_Y_Position + 12 + ; sta Sprite_Y_Position + 16 + ; sta Sprite_Y_Position + FireballOffset * 4 + ; sta Sprite_Y_Position + (FireballOffset+1) * 4 + + ldx ObjectMetasprite + beq @skipplayer + ldy #0 + jsr DrawMetasprite +@skipplayer: + + ; While we are here, lets draw the fireball/hammer sprites too + ldy #ProjectileOffset + sty $02 +@fireballLoop: + ldx ObjectMetasprite,y + beq @nofireball + jsr DrawMetasprite +@nofireball: + inc R2 + ldy R2 + cpy #ProjectileOffset+2 ; Draw two fireballs and one hammer + bne @fireballLoop + + ; jsr SwapToSavedPRG + ; double return + pla + pla + rts + + +; patch end of link's loading routine +.org $90DB ; sta $69DE + jsr SetupMarioControl +.reloc +SetupMarioControl: + sta $69DE + lda #8 + sta GameEngineSubroutine + lda #1 ; Ground area type + sta AreaType + lda #$28 ;store value here + sta VerticalForceDown ;for fractional movement downwards if necessary + rts + + +; Patch link's standard routine +.org $9041 + .word (GameRoutines) + +; .reloc +; NewLinkMarioProcess: + ; TODO: Process all the other stuff to decide what parts of mario update to run + ; jsr GameRoutines + ; rts +; .org $9041 +; .word (GameRoutines) + +.org $D4F3 ; patch the call to links main routine to set + jsr PatchLinkMain +.reloc +PatchLinkMain: + ldx #$00 + stx $14 + lda $de ; check if we can move + beq @notfrozen + jsr ProcFireball_Bubble ;process fireballs and air bubbles + rts +@notfrozen: + ; Do some checks that happen every frame + lda $0752 ; link is stuck in the mud + ora $0753 ; enemy is stuck in the mud + sta $0753 + ; disable for now +; lda $050C ; timer for link being hurt +; beq @notinjured +; rts +; @notinjured: +; lda $0503 ; sword slash timer +; bne @notwaiting +; rts +; @notwaiting: + ; decrement timers here cause lazy + ; Move the timers ahead by a frame as well + lda TimerControl ;if master timer control not set, decrement + beq DecTimers ;all frame and interval timers + dec TimerControl + bne NoDecTimers + DecTimers: + ldx #FRAME_TIMER_COUNT ;load end offset for end of frame timers + dec IntervalTimerControl ;decrement interval timer control, + bpl DecTimersLoop ;if not expired, only frame timers will decrement + lda #$14 + sta IntervalTimerControl ;if control for interval timers expired, + ldx #ALL_TIMER_COUNT ;interval timers will decrement along with frame timers + DecTimersLoop: + lda Timers,x ;check current timer + beq SkipExpTimer ;if current timer expired, branch to skip, + dec Timers,x ;otherwise decrement the current timer + SkipExpTimer: + dex ;move onto next timer + bpl DecTimersLoop ;do this until all timers are dealt with +NoDecTimers: + + ; Now do a few checks to see what state mario is in. + jsr $903A ; link main + + ; This has to come before the A/B buttons are switched over + jsr ProcFireball_Bubble ;process fireballs and air bubbles + + lda A_B_Buttons ;save current A and B button + sta PreviousA_B_Buttons ;into temp variable to be used on next frame + lda #$00 + sta Left_Right_Buttons ;nullify left and right buttons temp variable + + jsr SetPlayerDownstabbingHitbox + + ; jsr CheckSideviewTransition ; this is already run during collision detection + jsr PlayerGfxHandler + + rts + +.reloc +SetPlayerDownstabbingHitbox: + ; jsr $ed02 ; attempt to downstab while falling + jsr CheckForDownstab + bcc + + LDX #0 + JSR $ECEA + CLC + ADC #$25 - 4 ; - 2 cause I don't know + STA $0480 ; ,x + LDA $CC ; ,x + CLC + ADC #$10 ; + 8 ; + 8 cause UGH + STA $047E ; ,x + + + rts + +; Prevent recoil and playing the sword sound when hitting a well +.org $E268 + BCS $E28B +.org $E273 + jmp $E28B + +.org $E4A8 + jsr PatchFireballHitcheck +.segment "PRG7" +.reloc +PatchFireballHitcheck: + jsr $E694 ; original hitbox check + bcc @exit + ldy $11 ; fireball + lda #%10000000 + sta Fireball_State,y +@exit: + rts + +; .org $E558 +; rts ; disable link shield maybe? +; .org $E563 +; rts ; disable link shield maybe? +; .org $E579 +; rts ; disable link shield maybe? + +; Replace vanilla beam/fire code with our new +.org $d50d + ; jsr CustomProjectileCode + jmp *+3 + +; .reloc +; CustomProjectileCode: +; jsr ProcFireball_Bubble ;process fireballs and air bubbles +; rts +; .org $9370 +; ; TODO this is where link takes damage +; jmp GameRoutines + +; .org $937e +; nop ; don't do this sword attack check +; nop ; todo see what should be here instead +; nop +; ; there's a branch here from above to call the main game routine +; jmp GameRoutines + +; .org $93ba +; rts ; patch the link main routine to skip some unneeded stuff + +; Patch clearing out the ram for sideviews to also clear our new ram block +.org $C58F + jsr ClearCollisionRAM +.segment "PRG7" +.reloc +ClearCollisionRAM: + sta $07AF + ldy #$d0 + lda #0 +@loop: + sta $6340-1,y + sta $6340-1+$d0,y + sta $6340-1+$d0*2,y + sta $6340-1+$d0*3,y + dey + bne @loop + rts + +.org $C629 + jsr UpdateCollisionFromMap +.reloc +UpdateCollisionFromMap: + sta $0735 + ldx #$d0 +@loop: + lda $6000-1,x + jsr CheckForSolidMTiles + bcc @skip1 + sta $6340-1,x +@skip1: + lda $6000-1+$d0,x + jsr CheckForSolidMTiles + bcc @skip2 + sta $6340-1+$d0,x +@skip2: + lda $6000-1+$d0*2,x + jsr CheckForSolidMTiles + bcc @skip3 + sta $6340-1+$d0*2,x +@skip3: + lda $6000-1+$d0*3,x + jsr CheckForSolidMTiles + bcc @skip4 + sta $6340-1+$d0*3,x +@skip4: + dex + bne @loop + rts + +.reloc +SlightlyModifiedCollisionRoutine: + jsr SwapToSavedPRG + jsr $E095 ; skip ahead to near the end of the collision check + jmp SwapToPRG0 + +; .org $E073 + ; jsr PatchCollisionCheck + + +; .segment "PRG7" +; .reloc +; PatchCollisionCheck: + ; lda PlayerStandingMetatile + ; sta $03 ; will be checked to see what metatile you stepped on + ; jmp $E095 ; in collision but after the metatile collider + +; Recenter mario's bg collision hitbox when checking what position he is at. +; This changes where marios "center" metatile to match better with his visuals +.org $EAB8 +.byte $08 ; was $0f + +; Update spiky boi jump graphics to move out of the top bank +.org $ee7b +.byte $40, $40 + +; In vanilla, when link goes off screen to the left, it will go to page $ff, so the +; game has code that bumps the player up a page to counter that offset +; .org $e177 ; inc Player_PageLoc + ; nop + ; nop + +; Update palette locations +.ifdef DEMO_CODE + UPDATE_BYTE $37 @ $285a $2a0a $40af $40bf $40cf $40df $80af $80bf $80cf $80df $c0af $c0bf $c0cf $c0df $c0ef $100af $100bf $100cf $100df $140af $140bf $140cf $140df $17c19 $1c464 $1c47c + UPDATE_BYTE $27 @ $285b $2a0b $2a10 $40b0 $40c0 $40d0 $40e0 $80b0 $80c0 $80d0 $80e0 $c0b0 $c0c0 $c0d0 $c0e0 $c0f0 $100b0 $100c0 $100d0 $100e0 $140b0 $140c0 $140d0 $140e0 $17c1a $1c465 $1c47d + UPDATE_BYTE $16 @ $285C $40b1 $40c1 $40d1 $80e1 $80b1 $80c1 $80d1 $80e1 $c0b1 $c0c1 $c0d1 $c0e1 $100b1 $100c1 $100d1 $100e1 $140b1 $140c1 $140d1 $140e1 $17c1b $1c466 $1c47e +.else + UPDATE_BYTE $16 @ $285a $2a0a $40af $40bf $40cf $40df $80af $80bf $80cf $80df $c0af $c0bf $c0cf $c0df $c0ef $100af $100bf $100cf $100df $140af $140bf $140cf $140df $17c19 $1c464 $1c47c + UPDATE_BYTE $27 @ $285b $2a0b $2a10 $40b0 $40c0 $40d0 $40e0 $80b0 $80c0 $80d0 $80e0 $c0b0 $c0c0 $c0d0 $c0e0 $c0f0 $100b0 $100c0 $100d0 $100e0 $140b0 $140c0 $140d0 $140e0 $17c1a $1c465 $1c47d + UPDATE_BYTE $18 @ $285C $40b1 $40c1 $40d1 $80e1 $80b1 $80c1 $80d1 $80e1 $c0b1 $c0c1 $c0d1 $c0e1 $100b1 $100c1 $100d1 $100e1 $140b1 $140c1 $140d1 $140e1 $17c1b $1c466 $1c47e +.endif + +; regular palette color location +UPDATE_BYTE $18 $10ea +; shield spell color location +UPDATE_BYTE $02 @ $0e9e + + +; Replace writing registers directly with an output buffer to mix with the regular driver output. +; RESERVE SfxOutputBuffer, 12 + +; SND_SQUARE1_BUFFER = SfxOutputBuffer +; SND_SQUARE2_BUFFER = SfxOutputBuffer + 4 +; SND_NOISE_BUFFER = SfxOutputBuffer + 8 + +;.macro SFXInit +; +;; .if ::USE_VANILLA_SFX +; ldy #SFXMemoryEnd - SFXMemoryStart +;: sta SFXMemoryStart,y ;clear out memory used +; dey ;by the sound engines +; bpl :- +;; .endif +; +;.endmacro +; +;.macro SFXPlayback +; jsr SFXSoundEngine +;.endmacro + +;------------------------------------------------------------------------------------- +.segment "PRG6" +.org $9016 + jsr CheckMarioSq1Sfx + jsr CheckMarioSq2Sfx + jsr CheckMarioNoiseSfx + +.org $9022 + jsr ClearQueues + nop +.reloc +ClearQueues: + lda #0 + sta Square1SoundQueue + sta Square2SoundQueue + sta NoiseSoundQueue + rts + +.reloc +CheckMarioSq1Sfx: + jsr Square1SfxHandler + lda Square1SoundBuffer + beq @PlayingZ2 + lda #$ff + sta Z2Square1SoundBuffer + rts +@PlayingZ2: + lda #$ff + cmp Z2Square1SoundBuffer + bne @ActualZ1Sfx + ; this is the fake sfx id we used to signal that this was a mario buffer + ; but now we can clear it. + lda #0 + sta Z2Square1SoundBuffer + rts +@ActualZ1Sfx: + jmp $92F4 ; Original square 1 sfx from z2 +.reloc +CheckMarioSq2Sfx: + jsr Square2SfxHandler + lda Square2SoundBuffer + beq @PlayingZ2 + lda #$ff + sta Z2Square2SoundBuffer + rts +@PlayingZ2: + lda #$ff + cmp Z2Square2SoundBuffer + bne @ActualZ2Sfx + ; this is the fake sfx id we used to signal that this was a mario buffer + ; but now we can clear it. + lda #0 + sta Z2Square2SoundBuffer + rts +@ActualZ2Sfx: + jmp $9408 ; Original square 2 sfx from z2 +.reloc +CheckMarioNoiseSfx: + jsr NoiseSfxHandler + lda NoiseSoundBuffer + beq @PlayingZ2 + lda #$ff + sta Z2NoiseSoundBuffer + rts +@PlayingZ2: + lda #$ff + cmp Z2NoiseSoundBuffer + bne @ActualNoiseSfx + ; this is the fake sfx id we used to signal that this was a mario buffer + ; but now we can clear it. + lda #0 + sta Z2NoiseSoundBuffer + rts +@ActualNoiseSfx: + jmp $95A7 ; Original "complex" sfx from z2 + +; .reloc +; CheckMarioSfx: +; lda Square1SoundQueue +; ora MarioSquare1SoundBuffer +; ora Square2SoundQueue +; ora MarioSquare2SoundBuffer +; ora NoiseSoundQueue +; ora MarioNoiseSoundBuffer +; beq @NoMarioSfx +; jsr SFXSoundEngine +; sta MarioSquare1SoundPlaying +; jmp $901C +; @NoMarioSfx: +; ; P +; jsr $92F4 ; Zelda 2 square 1 sfx processing +; rts + +.segment "PRG7" +; Make mario state "on ground" when riding an elevator +.org $d8cd + jsr ElevatorMakeMarioStateStanding +.reloc +ElevatorMakeMarioStateStanding: + sta $0479 + sta Player_State + rts + +; Make mario "downstabbing" while falling +; .org $ED02 +; jsr CheckForDownstab +; bcc $ED1B +; nop + +; Make mario bounce off armored enemies +.org $E65E + jsr CheckForDownstab + bcc $E66A + LDA #$fc + STA $057D + rts +.assert * <= $E66A + +; Make mario bounce off armored enemies +.org $E714 + jsr CheckForDownstab + bcc $E723 + LDA $0B + BNE $E723 + LDA #$fc + STA $057D + nop +.assert * = $E723 + +.org $E9C0 + jsr InyIfDownstabbing + nop + nop + +.reloc +InyIfDownstabbing: + jsr CheckForDownstab + bcc + + iny + + + rts + +.reloc +CheckForDownstab: + lda Player_State + cmp #1 + bne @notjumping + lda Player_Y_Speed + beq @notjumping + bmi @notjumping + sec + rts +@notjumping: + clc + rts + +.segment "PRG3" +.org $9A42 + jmp * + 4 ; skip a hardcoded metasprite update when reading signs + diff --git a/RandomizerCore/Asm/z2mario/map.s b/RandomizerCore/Asm/z2mario/map.s new file mode 100644 index 000000000..40bb60d63 --- /dev/null +++ b/RandomizerCore/Asm/z2mario/map.s @@ -0,0 +1,167 @@ +.segment "PRG0" +; UPDATE_BYTE $00 @ $0cef + +;update mario palette in overworld +.org $8cdf + .byte $00 +; update raft tiles +.org $81C0 + lda #$b1 + sta $0201 + lda #$b3 + sta $0205 +; update raft tiles +.org $81DA + lda #$b3 + sta $0201 + lda #$b1 + sta $0205 + +.org $8728 + jmp DrawMarioMap +.reloc +DrawMarioMap: + sec + sbc #4 + ; LDA #$84 - 4 ; -6 + STA Sprite_X_Position + CLC + ADC #$08 + STA Sprite_X_Position+4 + LDA #$6D + STA Sprite_Y_Position + STA Sprite_Y_Position+4 + LDA #$73 + STA Sprite_Tilenumber+4 +; LDA $0562 +; CMP #$04 +; BCS L_8763 +; LSR A +; LDA #$00 +; ROR A +; LSR A +; STA Sprite_Attributes +; STA Sprite_Attributes+4 + LDX #$AF + lda SavedJoypadBits + and #Left_Dir+Right_Dir+Up_Dir+Down_Dir + ora $7D + BEQ L_875E +; PHP + ldx mapMarioIndex + lda mapMarioTimer + bne ++ + lda #7 + sta mapMarioTimer + inx + cpx #3 + bcc + + ldx #0 + + + stx mapMarioIndex + ++ + dec mapMarioTimer + lda MarioWalkingSprites,x + tax +; PLP +L_875E: + STX Sprite_Tilenumber +; PHP + DEX + DEX + STX Sprite_Tilenumber+4 + + ldx #$00 + stx $00 + lda $0562 + cmp #Down_Dir + bcs @upOrDown + ; flip the attribute if doing right + cmp #Right_Dir + bne + + ldx #$40 + stx $00 + + + stx Sprite_Attributes + stx Sprite_Attributes+4 + jmp @checkfortileflip +@upOrDown: + ; beq @down + ; up + lda mapMarioPrevDirection + ; flip the attribute if doing right + cmp #Right_Dir + bne + + ldx #$40 + stx $00 + + + stx Sprite_Attributes + stx Sprite_Attributes+4 + +; @down: +; ldx #$00 +; stx Sprite_Attributes +; stx Sprite_Attributes+4 +@checkfortileflip: + + ; flip the sprite tiles if the direction is right + lda $00 + bne @noflip + lda Sprite_Tilenumber + pha + lda Sprite_Tilenumber+4 + sta Sprite_Tilenumber + pla + sta Sprite_Tilenumber+4 +@noflip: + + ; store our most recent left/right press here + lda $0562 + and #Left_Dir+Right_Dir + beq + + sta mapMarioPrevDirection ++ + +; PLP +; BPL L_877C +;L_8763: +; LDX #$7B +; CMP #$04 +; BEQ L_876B +; DEX +; DEX +;L_876B: +; STX Sprite_Tilenumber +; LDX #$40 +; LDA $7D +; BEQ L_8779 +; AND #$08 +; BNE L_8779 +; TAX +;L_8779: +; STX Sprite_Attributes +L_877C: + LDX #$0E + LDY #$00 + STY $00 +L_8782: + LDA $00 + STA Sprite_Y_Position+8,Y + CLC + ADC #$10 + STA $00 + LDA #$7F + STA Sprite_Tilenumber+8,Y + LDA #$F8 + STA Sprite_X_Position+8,Y + LDA #$01 + STA Sprite_Attributes+8,Y + INY + INY + INY + INY + DEX + BPL L_8782 + RTS +MarioWalkingSprites: + .byte $ab, $a7, $a3 \ No newline at end of file diff --git a/RandomizerCore/Asm/z2mario/map_mario.chr b/RandomizerCore/Asm/z2mario/map_mario.chr new file mode 100644 index 0000000000000000000000000000000000000000..a3f7db9bf6eb3692d11c7bda6bce302231da23e5 GIT binary patch literal 2048 zcmeH@u}TCn5QgV2%gzaBQVCZ?W2fboQHIMa_yBUX<(7-6cg3~h3kbf#T4^PAww9Iy zzCi3EEXLfi*PKrvWSWo(U*?}rnBlNE00Dr;86}iCa&8K6(;QY+1;D;OzL-D<;6n&Z zUF}0RmD)Weq?B50@mMN}s0QT@3?Q97?|Z&YpV*3-8{2pWW)SLU<#6l4{}`*h+%77qX`fDQ1)BcM@7mO|^i1b^43>I6 zZuWnOw{LgxK3>zMtzo|5)BAe=_B%d)e#MjcLNhJlXB~VME>|{?4P*n^KsNB74EzGa Cv27dx literal 0 HcmV?d00001 diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s new file mode 100644 index 000000000..21c419b31 --- /dev/null +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -0,0 +1,604 @@ + + +;;;;;;;;;;;;;;;;; +; +; Custom metasprite defines for adding new metasprites to the game +; +; A metasprite is a collection of 1 or more 8x16 sprite tiles and their offset from the point on the screen where +; they should be drawn. Every moving character like koopas or mario will have many different metasprites associated +; with them for each of the different animation frames. +; +; In order to easily add and change existing metasprites, I've added several new helper macros so you can easily +; go wild with creativity when adding new characters to the game. No longer are you restricted to the original +; 16x24 box that the vanilla enemies are confined within. +; +; I wrote this because I'm sick of seeing hacks that just replace a goomba's graphics and call it a day. +; Where's all the hacks making awesome new enemies or even backporting modern mario enemies?! + +; Documentation: +; Because so many of the parameters are shared between each of the frames of an animation, +; this library simplifies by using variables that you define before calling any of the macros. + +; These implicit parameters have a default value that is overridable to customize the sprite in various ways +; In order to override any of the implicit parameters, you just need to prefix the variable with either just the Object +; name or both the Object and Animation names. If you prefix the parameter with the Object name, it will apply to all +; frames of the Object. To apply to only a single frame of the animation, then separate the object and animation with an +; underscore. + +; As an example, if you have a sprite where its position on the screen is supposed to be 8 pixels higher +; than its in game position, you can use the Y_OFFSET implicit parameter in order to shift it up just visually. +; So if you are working with the POWERUP_1_UP and want to shift it up slightly, make a new variable called +; `POWERUP_1_UP_Y_OFFSET = -8` before calling MetaspriteBox + +; As always, maybe the best documentation is just looking at the many many examples in this file used to define +; all of the metasprites in the vanilla game. + +;;;;;;;;;;;;;;;;;;;;;;;; +; Functions +; +; **** +; MetaspriteBox - Define a metasprite in either a 1x2 or 2x2 box of 8x16 sprites. +; Params: +; Object - string - Name of the metasprite. +; Animation - string - Name of the frame of animation +; Spr1, Spr2, Spr3, Spr4 - number - Each sprite tile in the animation +; Sprite Tile Params: These can be OR'd with the tile number to override the attributes just for one tile +; SPR_FLIP_H, SPR_FLIP_V - Force this particular tile to be flipped horizontally or vertically +; SPR_PALETTE_0, SPR_PALETTE_1, SPR_PALETTE_2, SPR_PALETTE_3 - Force this tile to use a specific palette +; +; Implicit Params: +; VRAM_OFFSET - number - Which sprite bank window to use. Adds the offset to each of the sprite tiles +; In general, the sprite bank windows are as follows: +; 0 - Mario +; 1 - Misc sprites (like powerups and coins) +; 2-3 - Enemy Sprites (Banked by area type on MMC3) +; 4-7 - MMC5 ONLY! Expanded sprite window to allow more sprites +; BANK - number - Bank number for the actual ROM bank used +; PALETTE - number - Base palette for the metasprite +; X_OFFSET - number - Number of pixels to offset the entire metasprite from the origin +; Y_OFFSET - number - Number of pixels to offset the entire metasprite from the origin +; NO_MIRROR - bool (1 or 0) - If 1, prevents generating a mirrored sprite for the left facing sprite. +; (Useful if the sprite cannot turn around and should always face in one direction) +; +; **** +; MetaspriteDuplicate - Reuse an existing metasprite. Useful for duplicate animation frames +; (This saves space because it will not duplicate the data) +; Params +; New Metasprite - string - Full name (object + animation) for the new metasprite +; Old Metasprite - string - Full name (object + animation) for the original metasprite +; +; **** +; MetaspriteData - Define a metasprite with a pointer to the data +; Params: +; Name - string - Full name (object + animation) for the new metasprite +; Left Data - word - Pointer to the data for the metasprite when facing left +; Right Data - word (optional) - Optional Pointer to the data for the metasprite when facing right. +; If missing, the Left data will be reused for the right facing sprite +; Data Format for the data is as follows +; .byte Length - 1 byte for total number of bytes in the metasprite. So if there are 4 sprites, then its 4 * 4 +; .struct SpriteData - repeated for each of the sprites in the metasprite +; .byte TileId - Tile Number for the sprite taking into account 8x16 mode and which window its in +; .byte Attribute +; .byte YPosition +; .byte XPosition +; .endstruct +; +; **** +; MetaspriteVramOffset - A small helper function to simplify combining the bank with the Tile ID +; when defining sprite data manually +; Params: +; Tile Id - 6 bit number - The tile number for the sprite +; Bank - SPRITE_BANK_N - The Sprite bank (0-3 for MMC3 or 0-7 for MMC5) to use for this sprite +; See also the docs for VRAM_OFFSET since this is the same thing +; +; **** +; MetaspriteReserve - Reserve a particular spot in the metasprite table but leave it to be defined until later +; Params: +; Name - string - Name of the reserved metasprite +; + +; Reserve Metasprite 0 as the "unused" sprite. This sprite won't be drawn to the screen + +.segment "PRG0" + +METASPRITE_BODY = 1 + +MetaspriteReserve "NULL" + +BIG_MARIO_VRAM_OFFSET = SPRITE_BANK_0 +BIG_MARIO_BANK = CHR_BIGMARIO +BIG_MARIO_PALETTE = $00 +BIG_MARIO_X_OFFSET = 8 +BIG_MARIO_Y_OFFSET = -8 +MetaspriteBox "BIG_MARIO", "STANDING", $00, $02, $20, $22 +MetaspriteBox "BIG_MARIO", "WALKING_1", $04, $06, $24, $26 +MetaspriteBox "BIG_MARIO", "WALKING_2", $08, $0a, $28, $2a +MetaspriteBox "BIG_MARIO", "WALKING_3", $0c, $0e, $2c, $2e +MetaspriteBox "BIG_MARIO", "SKIDDING", $10, $12, $30, $32 +MetaspriteBox "BIG_MARIO", "JUMPING", $14, $16, $34, $36 +MetaspriteBox "BIG_MARIO", "CROUCHING", $18, $1a, $38, $3a + +BIG_MARIO_SWIMMING_1_KICK_BANK = CHR_MARIOACTION +BIG_MARIO_SWIMMING_2_KICK_BANK = CHR_MARIOACTION +BIG_MARIO_SWIMMING_3_KICK_BANK = CHR_MARIOACTION +BIG_MARIO_SWIMMING_1_HOLD_BANK = CHR_MARIOACTION +BIG_MARIO_SWIMMING_2_HOLD_BANK = CHR_MARIOACTION +BIG_MARIO_SWIMMING_3_HOLD_BANK = CHR_MARIOACTION +MetaspriteBox "BIG_MARIO", "SWIMMING_1_KICK", $00, $02, $20, $22 +MetaspriteBox "BIG_MARIO", "SWIMMING_2_KICK", $04, $06, $20, $26 +MetaspriteBox "BIG_MARIO", "SWIMMING_3_KICK", $04, $06, $2a, $22 +MetaspriteBox "BIG_MARIO", "SWIMMING_1_HOLD", $00, $02, $24, $22 +MetaspriteBox "BIG_MARIO", "SWIMMING_2_HOLD", $04, $06, $24, $26 +MetaspriteBox "BIG_MARIO", "SWIMMING_3_HOLD", $04, $06, $28, $22 + +FIRE_MARIO_OFFSET = METASPRITES_COUNT - METASPRITE_BIG_MARIO_STANDING +FIRE_MARIO_VRAM_OFFSET = SPRITE_BANK_0 +FIRE_MARIO_BANK = CHR_MARIOACTION +FIRE_MARIO_PALETTE = $00 +FIRE_MARIO_X_OFFSET = 8 +FIRE_MARIO_Y_OFFSET = -8 +MetaspriteBox "FIRE_MARIO", "STANDING", $00, $02, $2c, $2e +; MetaspriteBox "BIG_MARIO", "FIRE_WALKING_1", $00, $02, $2c, $2e +MetaspriteDuplicate "FIRE_MARIO_WALKING_1", "FIRE_MARIO_STANDING" +MetaspriteBox "FIRE_MARIO", "WALKING_2", $00, $02, $30, $32 +MetaspriteBox "FIRE_MARIO", "WALKING_3", $00, $02, $14, $16 +MetaspriteBox "FIRE_MARIO", "SKIDDING", $00, $02, $34, $36 +MetaspriteBox "FIRE_MARIO", "JUMPING", $00, $02, $10, $12 +FIRE_MARIO_CROUCHING_BANK = CHR_SMALLFIRE +MetaspriteBox "FIRE_MARIO", "CROUCHING", $00, $02, $18, $1a + +MetaspriteBox "FIRE_MARIO", "SWIMMING_1_KICK", $00, $02, $20, $22 +MetaspriteBox "FIRE_MARIO", "SWIMMING_2_KICK", $04, $06, $20, $26 +MetaspriteBox "FIRE_MARIO", "SWIMMING_3_KICK", $04, $06, $2a, $22 +MetaspriteBox "FIRE_MARIO", "SWIMMING_1_HOLD", $00, $02, $24, $22 +MetaspriteBox "FIRE_MARIO", "SWIMMING_2_HOLD", $04, $06, $24, $26 +MetaspriteBox "FIRE_MARIO", "SWIMMING_3_HOLD", $04, $06, $28, $22 + +BIG_MARIO_CLIMBING_1_BANK = CHR_MARIOACTION +BIG_MARIO_CLIMBING_2_BANK = CHR_MARIOACTION +MetaspriteBox "BIG_MARIO", "CLIMBING_1", $00, $02, $08, $0a +MetaspriteBox "BIG_MARIO", "CLIMBING_2", $04, $06, $0c, $0e + +SMALL_MARIO_VRAM_OFFSET = SPRITE_BANK_0 +SMALL_MARIO_BANK = CHR_SMALLMARIO +SMALL_MARIO_PALETTE = $00 +SMALL_MARIO_X_OFFSET = 8 +SMALL_MARIO_Y_OFFSET = 8 +MetaspriteBox "SMALL_MARIO", "STANDING", $00, $02 +MetaspriteBox "SMALL_MARIO", "WALKING_1", $04, $06 +MetaspriteBox "SMALL_MARIO", "WALKING_2", $08, $0a +MetaspriteBox "SMALL_MARIO", "WALKING_3", $0c, $0e +MetaspriteBox "SMALL_MARIO", "SKIDDING", $10, $12 +MetaspriteBox "SMALL_MARIO", "JUMPING", $14, $16 + +MetaspriteBox "SMALL_MARIO", "SWIMMING_1_KICK", $20, $22 +MetaspriteBox "SMALL_MARIO", "SWIMMING_2_KICK", $28, $2a +MetaspriteBox "SMALL_MARIO", "SWIMMING_3_KICK", $30, $32 +MetaspriteBox "SMALL_MARIO", "SWIMMING_1_HOLD", $24, $26 +MetaspriteBox "SMALL_MARIO", "SWIMMING_2_HOLD", $2c, $2e +MetaspriteDuplicate "SMALL_MARIO_SWIMMING_3_HOLD", "SMALL_MARIO_SWIMMING_3_KICK" + +SMALL_FIRE_VRAM_OFFSET = SPRITE_BANK_0 +SMALL_FIRE_BANK = CHR_SMALLFIRE +SMALL_FIRE_PALETTE = $00 +SMALL_FIRE_X_OFFSET = 8 +SMALL_FIRE_Y_OFFSET = -8 +MetaspriteBox "SMALL_FIRE", "STANDING", $00, $02, $20, $22 +MetaspriteBox "SMALL_FIRE", "WALKING_1", $00, $02, $04, $06 +MetaspriteBox "SMALL_FIRE", "WALKING_2", $00, $02, $08, $0a +MetaspriteBox "SMALL_FIRE", "WALKING_3", $00, $02, $0c, $0e +MetaspriteBox "SMALL_FIRE", "SKIDDING", $00, $02, $10, $12 +MetaspriteBox "SMALL_FIRE", "JUMPING", $00, $02, $14, $16 + +SWIMMING_ANIMATION_FRAME_COUNT = METASPRITE_BIG_MARIO_SWIMMING_1_HOLD - METASPRITE_BIG_MARIO_SWIMMING_1_KICK +MetaspriteBox "SMALL_FIRE", "SWIMMING_1_KICK", $00, $02, $34, $36 +MetaspriteBox "SMALL_FIRE", "SWIMMING_2_KICK", $00, $02, $30, $32 +MetaspriteBox "SMALL_FIRE", "SWIMMING_3_KICK", $00, $02, $34, $36 +MetaspriteBox "SMALL_FIRE", "SWIMMING_1_HOLD", $00, $02, $18, $1a +MetaspriteBox "SMALL_FIRE", "SWIMMING_2_HOLD", $00, $02, $2c, $2e +MetaspriteDuplicate "SMALL_FIRE_SWIMMING_3_HOLD", "SMALL_FIRE_SWIMMING_3_KICK" + +MetaspriteBox "SMALL_MARIO", "CLIMBING_1", $18, $1a +MetaspriteBox "SMALL_MARIO", "CLIMBING_2", $38, $3a +MetaspriteBox "SMALL_MARIO", "DEATH", $34, $36 + +SMALL_MARIO_GROW_STANDING_BANK = CHR_SMALLMARIO +BIG_MARIO_GROW_INTERMEDIATE_BANK = CHR_MARIOACTION +BIG_MARIO_GROW_STANDING_BANK = CHR_BIGMARIO +MetaspriteDuplicate "SMALL_MARIO_GROW_STANDING", "SMALL_MARIO_STANDING" +MetaspriteBox "BIG_MARIO", "GROW_INTERMEDIATE", $18, $1a, $38, $3a +MetaspriteDuplicate "BIG_MARIO_GROW_STANDING", "BIG_MARIO_STANDING" + +; Glitchy looking sprite used when mario fires a fireball with no momentum in the water. +FIRE_MARIO_SWIMMING_STILL_1_BANK = CHR_MARIOACTION +MetaspriteDuplicate "FIRE_MARIO_SWIMMING_STILL_1", "FIRE_MARIO_STANDING" +; This second sprite is a unique one with the "Hold" foot sprite overwriting the walking sprite +FIRE_MARIO_SWIMMING_STILL_2_BANK = CHR_MARIOACTION +MetaspriteBox "FIRE_MARIO", "SWIMMING_STILL_2", $00, $02, $24, $2e +; Glitchy looking sprite used when mario fires a fireball with no momentum in the water. +SMALL_FIRE_SWIMMING_STILL_1_BANK = CHR_SMALLFIRE +MetaspriteDuplicate "SMALL_FIRE_SWIMMING_STILL_1", "FIRE_MARIO_STANDING" +; This second sprite is a unique one with the "Hold" foot sprite overwriting the walking sprite +SMALL_FIRE_SWIMMING_STILL_2_BANK = CHR_SMALLFIRE +MetaspriteBox "SMALL_FIRE", "SWIMMING_STILL_2", $00, $02, $28, $2a + +; Update this with the first and last metasprite if more are added before or after +TOTAL_MARIO_METASPRITES = METASPRITE_SMALL_FIRE_SWIMMING_STILL_2 - METASPRITE_BIG_MARIO_STANDING + 1 + +POWERUP_VRAM_OFFSET = SPRITE_BANK_1 +; POWERUP_Y_OFFSET = 8 +POWERUP_1UP_PALETTE = $01 +POWERUP_STAR_PALETTE = $00 +POWERUP_FIREFLOWER_PALETTE = $00 +POWERUP_FIREFLOWER_Y_OFFSET = -8 +POWERUP_MUSHROOM_PALETTE = $02 +POWERUP_1UP_NO_MIRROR = 1 +POWERUP_MUSHROOM_NO_MIRROR = 1 +MetaspriteBox "POWERUP", "STAR", $34, $36 +; Force the lower two parts of the fireflower to be palette two by using the override +MetaspriteBox "POWERUP", "FIREFLOWER", $38, $38 | SPR_FLIP_H , $3a | SPR_PALETTE_1, $3a | SPR_FLIP_H | SPR_PALETTE_1 +MetaspriteBox "POWERUP", "MUSHROOM", $3c, $3e +MetaspriteBox "POWERUP", "1UP", $3c, $3e + +FIREBALL_VRAM_OFFSET = SPRITE_BANK_0 +FIREBALL_PALETTE = $00 +FIREBALL_X_OFFSET = 8 +FIREBALL_Y_OFFSET = -12 +EXPLOSION_VRAM_OFFSET = SPRITE_BANK_1 +EXPLOSION_PALETTE = $00 +EXPLOSION_X_OFFSET = 8 +EXPLOSION_Y_OFFSET = -12 ; Vanilla offsets the explosion by -4. The other -8 is to account for position due to 8x16 sprites? +MetaspriteBox "FIREBALL", "FRAME_1", $3c +MetaspriteBox "FIREBALL", "FRAME_2", $3e +MetaspriteBox "EXPLOSION", "FRAME_1", $26, $26 | SPR_FLIP_H +MetaspriteBox "EXPLOSION", "FRAME_2", $28, $28 | SPR_FLIP_H +MetaspriteBox "EXPLOSION", "FRAME_3", $2a, $2a | SPR_FLIP_H +; Firebars will still draw Fireballs manually for now + +COIN_VRAM_OFFSET = SPRITE_BANK_1 +COIN_PALETTE = $02 +MetaspriteBox "COIN", "FRAME_1", $2c +MetaspriteBox "COIN", "FRAME_2", $2e +MetaspriteBox "COIN", "FRAME_3", $30 +MetaspriteBox "COIN", "FRAME_4", $32 + +MISC_VRAM_OFFSET = SPRITE_BANK_1 +MISC_PALETTE = $03 +MISC_Y_OFFSET = -8 + +; The brick and block sprite palettes are set by the code +MISC_BRICK_GROUND_PALETTE = $00 +MISC_BRICK_OTHER_PALETTE = $00 ; The brick palette is set by the code +MISC_BLOCK_PALETTE = $00 ; The block palette is set by the code +MetaspriteBox "MISC", "BRICK_OTHER", $20, $20 +MetaspriteBox "MISC", "BRICK_GROUND", $22, $22 +MetaspriteBox "MISC", "BLOCK", $24, $24 | SPR_FLIP_H +; drawn manually for now until i figure out if i have frames for the brick +; break animation with just metasprites +; MetaspriteBox "MISC", "CHUNK", $16 +MISC_BUBBLE_PALETTE = $02 +MetaspriteBox "MISC", "BUBBLE", $18 +MetaspriteBox "MISC", "SMALL_OVERLAY", $1e, $1e +MetaspriteBox "MISC", "LARGE_OVERLAY", $1e, $1e, $1e, $1e +MISC_STAR_FLAG_VRAM_OFFSET = SPRITE_BANK_2 +MISC_STAR_FLAG_PALETTE = $02 +MetaspriteBox "MISC", "STAR_FLAG", $3c, $3e +; MISC_FLAGPOLE_FLAG_VRAM_OFFSET = SPRITE_BANK_3 +; MISC_FLAGPOLE_FLAG_PALETTE = $01 +; MetaspriteBox "MISC", "FLAGPOLE_FLAG", $32, $34 + +; PLATFORM_VRAM_OFFSET = SPRITE_BANK_1 +; PLATFORM_PALETTE = $02 +; MetaspriteReserve "PLATFORM_GIRDER_SMALL" +; MetaspriteReserve "PLATFORM_GIRDER_SMALL_FRAME_2" +; MetaspriteReserve "PLATFORM_GIRDER_LARGE" +; MetaspriteReserve "PLATFORM_GIRDER_LARGE_FRAME_2" +; MetaspriteReserve "PLATFORM_CLOUD_SMALL" +; MetaspriteReserve "PLATFORM_CLOUD_SMALL_FRAME_2" +; MetaspriteReserve "PLATFORM_CLOUD_LARGE" +; MetaspriteReserve "PLATFORM_CLOUD_LARGE_FRAME_2" + +; NUMBER_VRAM_OFFSET = SPRITE_BANK_1 +; NUMBER_PALETTE = $02 +; NUMBER_Y_OFFSET = -16 + +; MetaspriteBox "NUMBER", "100", $06, $10 +; MetaspriteBox "NUMBER", "200", $08, $10 +; MetaspriteBox "NUMBER", "400", $0a, $10 +; MetaspriteBox "NUMBER", "500", $0c, $10 +; MetaspriteBox "NUMBER", "800", $0e, $10 +; MetaspriteBox "NUMBER", "1000", $06, $04 +; MetaspriteBox "NUMBER", "2000", $08, $04 +; MetaspriteBox "NUMBER", "4000", $0a, $04 +; MetaspriteBox "NUMBER", "5000", $0c, $04 +; MetaspriteBox "NUMBER", "8000", $0e, $04 +; MetaspriteBox "NUMBER", "1UP", $12, $14 + + +; GOOMBA_VRAM_OFFSET = SPRITE_BANK_2 +; GOOMBA_PALETTE = $03 +; GOOMBA_DEAD_Y_OFFSET = $08 +; MetaspriteBox "GOOMBA", "WALKING_1", $22, $24 +; MetaspriteBox "GOOMBA", "WALKING_2", $24 | SPR_FLIP_H, $22 | SPR_FLIP_H +; MetaspriteBox "GOOMBA", "DEAD", $26, $26 | SPR_FLIP_H + + +; KOOPA_VRAM_OFFSET = SPRITE_BANK_2 +; KOOPA_Y_OFFSET = -8 +; KOOPA_PALETTE = $00 ; Don't set a palette here so we can do red or green koopa later +; ; Koopa shell is offset by 2 pixels for the right side up animation. +; ; We account for this in the code by adding 2 with the vertical flip flag +; KOOPA_SHELL_Y_OFFSET = 0 +; KOOPA_SHELL_REVIVE_Y_OFFSET = 0 +; MetaspriteBox "KOOPA", "WALKING_1", $38, $12, $30, $32 +; MetaspriteBox "KOOPA", "WALKING_2", $18, $16, $34, $36 +; MetaspriteBox "KOOPA", "SHELL", $1a, $1a | SPR_FLIP_H +; MetaspriteBox "KOOPA", "SHELL_REVIVE", $3a, $3a | SPR_FLIP_H +; MetaspriteBox "KOOPA", "FLYING_1", $10, $12, $30, $32 +; MetaspriteBox "KOOPA", "FLYING_2", $14, $16, $34, $36 + + +; PIRANHA_VRAM_OFFSET = SPRITE_BANK_3 +; PIRANHA_PALETTE = $01 +; PIRANHA_Y_OFFSET = -8 +; MetaspriteBox "PIRANHA", "MOUTH_OPEN", $1c, $1c | SPR_FLIP_H, $3c, $3c | SPR_FLIP_H +; MetaspriteBox "PIRANHA", "MOUTH_CLOSED", $1e, $1e | SPR_FLIP_H, $3e, $3e | SPR_FLIP_H + +; BULLET_VRAM_OFFSET = SPRITE_BANK_2 +; BULLET_PALETTE = $03 +; MetaspriteBox "BULLET", "BILL", $1c, $1e + +; PODOBOO_VRAM_OFFSET = SPRITE_BANK_4 +; PODOBOO_PALETTE = $02 +; MetaspriteBox "PODOBOO", "UP", $18, $18 | SPR_FLIP_H + + +; BUZZY_BEETLE_VRAM_OFFSET = SPRITE_BANK_2 +; BUZZY_BEETLE_PALETTE = $03 +; ; See note about koopa shell for this offset +; BUZZY_BEETLE_SHELL_Y_OFFSET = 0 +; MetaspriteBox "BUZZY_BEETLE", "WALKING_1", $00, $02 +; MetaspriteBox "BUZZY_BEETLE", "WALKING_2", $04, $06 +; MetaspriteBox "BUZZY_BEETLE", "SHELL", $20, $20 | SPR_FLIP_H + + +; CHEEP_CHEEP_VRAM_OFFSET = SPRITE_BANK_2 +; CHEEP_CHEEP_PALETTE = $00 +; MetaspriteBox "CHEEP_CHEEP", "SWIM_1", $2c, $2e +; MetaspriteBox "CHEEP_CHEEP", "SWIM_2", $2a, $2e + + +; BLOOPER_VRAM_OFFSET = SPRITE_BANK_3 +; BLOOPER_PALETTE = $03 +; BLOOPER_Y_OFFSET = -8 +; BLOOPER_SWIM_2_Y_OFFSET = (-8 + 3) ; original code adds 3 px to the y position +; MetaspriteBox "BLOOPER", "SWIM_1", $1a, $1a | SPR_FLIP_H, $3a, $3a | SPR_FLIP_H +; MetaspriteBox "BLOOPER", "SWIM_2", $18, $18 | SPR_FLIP_H + +; HAMMER_VRAM_OFFSET = SPRITE_BANK_1 +; HAMMER_FRAME_1_X_OFFSET = 4 +; HAMMER_FRAME_1_Y_OFFSET = -8 +; HAMMER_FRAME_2_X_OFFSET = 0 +; HAMMER_FRAME_2_Y_OFFSET = -8 +; HAMMER_PALETTE = $03 +; MetaspriteBox "HAMMER", "FRAME_1", $1e +; MetaspriteBox "HAMMER", "FRAME_2", $00, $02 + +; HAMMER_BRO_VRAM_OFFSET = SPRITE_BANK_3 +; HAMMER_BRO_PALETTE = $01 +; HAMMER_BRO_Y_OFFSET = -8 +; MetaspriteBox "HAMMER_BRO", "WALK_1", $00, $02, $20, $22 +; MetaspriteBox "HAMMER_BRO", "WALK_2", $08, $0a, $24, $26 +; MetaspriteBox "HAMMER_BRO", "THROW_1", $04, $06, $20, $22 +; MetaspriteBox "HAMMER_BRO", "THROW_2", $04, $06, $24, $26 + + +; LAKITU_VRAM_OFFSET = SPRITE_BANK_3 +; LAKITU_PALETTE = $01 +; LAKITU_NORMAL_Y_OFFSET = -16 +; LAKITU_THROWING_Y_OFFSET = 0 +; MetaspriteBox "LAKITU", "NORMAL", $0c, $0e, $2c, $2e +; MetaspriteBox "LAKITU", "THROWING", $30, $30 | SPR_FLIP_H + + +; SPINY_VRAM_OFFSET = SPRITE_BANK_2 +; SPINY_PALETTE = $02 +; SPINY_EGG_1_VRAM_OFFSET = SPRITE_BANK_3 +; SPINY_EGG_2_VRAM_OFFSET = SPRITE_BANK_3 +; MetaspriteBox "SPINY", "WALK_1", $08, $0a +; MetaspriteBox "SPINY", "WALK_2", $0c, $0e +; MetaspriteBox "SPINY", "EGG_1", $10, $10 | SPR_FLIP_H | SPR_FLIP_V +; MetaspriteBox "SPINY", "EGG_2", $12, $12 | SPR_FLIP_H | SPR_FLIP_V + + +; JUMPSPRING_VRAM_OFFSET = SPRITE_BANK_3 +; JUMPSPRING_PALETTE = $02 +; JUMPSPRING_Y_OFFSET = -8 +; MetaspriteBox "JUMPSPRING", "FRAME_1", $16, $16 | SPR_FLIP_H , $36, $36 | SPR_FLIP_H +; MetaspriteBox "JUMPSPRING", "FRAME_2", $38, $38 | SPR_FLIP_H +; MetaspriteBox "JUMPSPRING", "FRAME_3", $14, $14 + +; BOWSER_VRAM_OFFSET = SPRITE_BANK_4 +; BOWSER_PALETTE = $01 +; BOWSER_Y_OFFSET = -8 +; MetaspriteBox "BOWSER", "FRONT_MOUTH_OPEN", $08, $0a, $28 +; MetaspriteBox "BOWSER", "FRONT_MOUTH_CLOSED", $0c, $0e, $28 +; MetaspriteBox "BOWSER", "REAR_WALK_1", $04, $06, $20, $22 +; MetaspriteBox "BOWSER", "REAR_WALK_2", $04, $06, $24, $26 + +; MetaspriteReserve "BOWSER_FLAME" + +; PEACH_VRAM_OFFSET = SPRITE_BANK_4 +; PEACH_PALETTE = $02 +; PEACH_Y_OFFSET = -8 +; MetaspriteBox "PEACH", "STANDING", $14, $16, $34, $34 | SPR_FLIP_H +; TOAD_VRAM_OFFSET = SPRITE_BANK_4 +; TOAD_PALETTE = $02 +; TOAD_Y_OFFSET = -8 +; MetaspriteBox "TOAD", "STANDING", $10, $10 | SPR_FLIP_H, $12, $12 | SPR_FLIP_H + +; .if ::MOUSE_DISPLAY_CURSOR +; MOUSE_VRAM_OFFSET = SPRITE_BANK_5 +; MOUSE_PALETTE = $02 +; MOUSE_NO_MIRROR = 1 +; MOUSE_Y_OFFSET = -8 +; MetaspriteBox "MOUSE", "POINTER", $00, $02 +; MetaspriteBox "MOUSE", "PINCH", $04, $06 +; MetaspriteBox "MOUSE", "DISCONNECTED", $08, $0a +; .endif + + +; Extra sprite tiles that aren't using metasprites + +; These aren't actually sprite tiles, they are the replacement tile for +; the rope when using a balance platform +BALANCE_PLATFORM_ROPE_1 = $7d +BALANCE_PLATFORM_ROPE_2 = $7e + +; Brick chunks have a really custom movement and making them a metasprite +; wasn't worth the trouble for now. +BRICK_CHUNK_TILE = MetaspriteVramOffset {$16}, {SPRITE_BANK_1} + +; Used by the "small platform" which is the elevator platforms +; PLATFORM_GIRDER = $5b +PLATFORM_GIRDER = MetaspriteVramOffset {$1a}, {SPRITE_BANK_1} + +; Vine drawing is probably possible to do as a metasprite but not worth the trouble +VINE_TILE_1 = MetaspriteVramOffset {$28}, {SPRITE_BANK_3} +VINE_TILE_2 = MetaspriteVramOffset {$2a}, {SPRITE_BANK_3} + +; Used when drawing firebars (not when shooting fireballs which are metasprites) +FIREBALL_TILE1 = MetaspriteVramOffset {$3c}, {SPRITE_BANK_0} +FIREBALL_TILE2 = MetaspriteVramOffset {$3e}, {SPRITE_BANK_0} + + +;;;;;;;;;;; +; NULL Metasprite needs to be reserved in slot 0 to allow disabling drawing a sprite before its deleted + +MetaspriteData "METASPRITE_NULL", $0000 + +;;;;;;;;;;; +; Girder Platform + +; Y_OFFSET .set 0 +; X_OFFSET .set 0 +; PALETTE .set 2 + +; MetaspriteData "METASPRITE_PLATFORM_GIRDER_SMALL", MetaspritePlatformGirderSmall +; MetaspriteData "METASPRITE_PLATFORM_GIRDER_SMALL_FRAME_2", MetaspritePlatformGirderSmallFrame2 + +; MetaspritePlatformGirderSmall: +; .byte 4 * 4 +; .repeat 4, I +; .byte MetaspriteVramOffset{$1a}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (I * 8) +; .endrepeat + +; MetaspritePlatformGirderSmallFrame2: +; .byte 4 * 4 +; .repeat 4, I +; .byte MetaspriteVramOffset{$1a}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (((I + 2) .mod 4) * 8) +; .endrepeat + +; MetaspriteData "METASPRITE_PLATFORM_GIRDER_LARGE", MetaspritePlatformGirderLarge +; MetaspriteData "METASPRITE_PLATFORM_GIRDER_LARGE_FRAME_2", MetaspritePlatformGirderLargeFrame2 + +; MetaspritePlatformGirderLarge: +; .byte 6 * 4 +; .repeat 6, I +; .byte MetaspriteVramOffset{$1a}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (I * 8) +; .endrepeat + +; MetaspritePlatformGirderLargeFrame2: +; .byte 6 * 4 +; .repeat 6, I +; .byte MetaspriteVramOffset{$1a}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (((I + 3) .mod 6) * 8) +; .endrepeat + +; ;;;;;;;;;;; +; ; Cloud Platform + +; Y_OFFSET .set 0 +; X_OFFSET .set 0 +; PALETTE .set 2 + +; MetaspriteData "METASPRITE_PLATFORM_CLOUD_SMALL", MetaspritePlatformCloudSmall +; MetaspriteData "METASPRITE_PLATFORM_CLOUD_SMALL_FRAME_2", MetaspritePlatformCloudSmallFrame2 + +; MetaspritePlatformCloudSmall: +; .byte 4 * 4 +; .repeat 4, I +; .byte MetaspriteVramOffset{$1c}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (I * 8) +; .endrepeat + +; MetaspritePlatformCloudSmallFrame2: +; .byte 4 * 4 +; .repeat 4, I +; .byte MetaspriteVramOffset{$1c}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (((I + 2) .mod 4) * 8) +; .endrepeat + +; MetaspriteData "METASPRITE_PLATFORM_CLOUD_LARGE", MetaspritePlatformCloudLarge +; MetaspriteData "METASPRITE_PLATFORM_CLOUD_LARGE_FRAME_2", MetaspritePlatformCloudLargeFrame2 + +; MetaspritePlatformCloudLarge: +; .byte 6 * 4 +; .repeat 6, I +; .byte MetaspriteVramOffset{$1c}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (I * 8) +; .endrepeat + +; MetaspritePlatformCloudLargeFrame2: +; .byte 6 * 4 +; .repeat 6, I +; .byte MetaspriteVramOffset{$1c}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (((I + 3) .mod 6) * 8) +; .endrepeat + + +; Y_OFFSET .set -4 +; X_OFFSET .set 0 +; PALETTE .set 2 +; MetaspriteData "METASPRITE_BOWSER_FLAME", MetaspriteBowserFlame, MetaspriteBowserFlame +; MetaspriteBowserFlame: +; .byte 3 * 4 +; .byte MetaspriteVramOffset{$2d}, {SPRITE_BANK_4}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET +; .byte MetaspriteVramOffset{$2f}, {SPRITE_BANK_4}, PALETTE, 0 + Y_OFFSET, 8 + X_OFFSET +; .byte MetaspriteVramOffset{$31}, {SPRITE_BANK_4}, PALETTE, 0 + Y_OFFSET, 16 + X_OFFSET + + + + + +;;;; +; Internal use +; Generate the lookup tables for the metasprites used by the renderer. +; All metasprites must be defined before this part + +MetaspriteTableLeftLo: +.repeat METASPRITES_COUNT, I + .byte .ident(.sprintf("METASPRITE_LEFT_%d_LO", I)) +.endrepeat +MetaspriteTableLeftHi: +.repeat METASPRITES_COUNT, I + .byte .ident(.sprintf("METASPRITE_LEFT_%d_HI", I)) +.endrepeat + +MetaspriteTableRightLo: +.repeat METASPRITES_COUNT, I + .byte .ident(.sprintf("METASPRITE_RIGHT_%d_LO", I)) +.endrepeat +MetaspriteTableRightHi: +.repeat METASPRITES_COUNT, I + .byte .ident(.sprintf("METASPRITE_RIGHT_%d_HI", I)) +.endrepeat + +PlayerBankTable = PlayerBankTableReal - METASPRITE_BIG_MARIO_STANDING +PlayerBankTableReal: +.repeat TOTAL_MARIO_METASPRITES+METASPRITE_BIG_MARIO_STANDING, I +.if I >= METASPRITE_BIG_MARIO_STANDING + .byte .lobyte(.ident(.sprintf("METASPRITE_%d_BANK", I))) +.endif +.endrepeat +TOTAL_MARIO_SPRITE_BANK = * - PlayerBankTableReal +.assert TOTAL_MARIO_SPRITE_BANK = TOTAL_MARIO_METASPRITES, error, .sprintf("Total number of Mario Metasprites (%d) does not match the mario bank table (%d)! Update the bank table to match ", TOTAL_MARIO_METASPRITES, TOTAL_MARIO_SPRITE_BANK) + + + + diff --git a/RandomizerCore/Asm/z2mario/metasprite_engine.s b/RandomizerCore/Asm/z2mario/metasprite_engine.s new file mode 100644 index 000000000..cf135a0fe --- /dev/null +++ b/RandomizerCore/Asm/z2mario/metasprite_engine.s @@ -0,0 +1,722 @@ + +.segment "PRG0" + +PRINT_METASPRITE_IDS = 0 + +METASPRITES_COUNT .set 0 + +PATTERN_TABLE_0 = $00 +PATTERN_TABLE_1 = $01 +SPRITE_BANK_0 = $00 | PATTERN_TABLE_0 +SPRITE_BANK_1 = $40 | PATTERN_TABLE_0 +SPRITE_BANK_2 = $80 | PATTERN_TABLE_0 +SPRITE_BANK_3 = $c0 | PATTERN_TABLE_0 +SPRITE_BANK_4 = $00 | PATTERN_TABLE_1 +SPRITE_BANK_5 = $40 | PATTERN_TABLE_1 +SPRITE_BANK_6 = $80 | PATTERN_TABLE_1 +SPRITE_BANK_7 = $c0 | PATTERN_TABLE_1 + + +MSPR_VERTICAL_FLIP = %10000000 +MSPR_OFFSET_MASK = %00111111 +.define MetaspriteOffset(b) 0+((b) & MSPR_OFFSET_MASK) + +; Custom values to signify that this sprite should be flipped in the metasprite +SPR_FLIP_H = %01000000 << 8 +SPR_FLIP_V = %10000000 << 8 +; Can be used to force a sprite palette. If NO_PALETTE is set, then the palette value +; from the object in the game will not be applied, and this value will be forced instead +SPR_NO_PALETTE = %00100000 << 8 +SPR_PALETTE_0 = SPR_NO_PALETTE | %00000000 << 8 +SPR_PALETTE_1 = SPR_NO_PALETTE | %00000001 << 8 +SPR_PALETTE_2 = SPR_NO_PALETTE | %00000010 << 8 +SPR_PALETTE_3 = SPR_NO_PALETTE | %00000011 << 8 + + +.define MetaspriteVramOffset(b, vram) (((b) & %00111111) | vram) + +.macro MetaspriteBoxBody Object, Animation, Direction, VramOffset, Palette, XOffset, YOffset, Spr1, Spr2, Spr3, Spr4 +.local X1, X2, Y1, Y2, Size +.local Tile1, Tile2, Tile3, Tile4 +.local Attr1, Attr2, Attr3, Attr4 +.local DrawSprite1, DrawSprite2, DrawSprite3, DrawSprite4 + +X1 = 0+XOffset +X2 = 8+XOffset +Y1 = 8+YOffset +Y2 = 24+YOffset +Size .set 1 + +DrawSprite2 .set 0 +DrawSprite3 .set 0 +DrawSprite4 .set 0 + +DrawSprite1 .set 1 +Tile1 .set MetaspriteVramOffset(Spr1) >> 8)) +.ifnblank Spr2 +Attr2 .set (Palette | ((>Spr2) >> 8)) +.endif +.ifnblank Spr3 +Attr3 .set (Palette | ((>Spr3) >> 8)) +.endif +.ifnblank Spr4 +Attr4 .set (Palette | ((>Spr4) >> 8)) +.endif + +.if .xmatch(Direction, "LEFT") +; .out .sprintf("Switching tile and attr %s %s %s", Object, Animation, Direction) +.ifnblank Spr2 + Tmp .set Tile1 + Tile1 .set Tile2 + Tile2 .set Tmp + Tmp .set Attr1 + Attr1 .set Attr2 + Attr2 .set Tmp + Attr1 .set Attr1 ^ $40 + Attr2 .set Attr2 ^ $40 +.endif +.if .blank(Spr4) .and(.not .blank(Spr3)) + DrawSprite4 .set 1 + DrawSprite3 .set 0 + Attr4 .set Attr3 ^ $40 + Tile4 .set Tile3 +.endif +.ifnblank Spr4 + Tmp .set Tile3 + Tile3 .set Tile4 + Tile4 .set Tmp + Tmp .set Attr3 + Attr3 .set Attr4 + Attr4 .set Tmp + Attr3 .set Attr3 ^ $40 + Attr4 .set Attr4 ^ $40 +.endif +.endif + +.ident( .sprintf("MetaspriteData_%s_%s_%s", Object, Animation, Direction) ): + .byte (Size * 4) +.if DrawSprite1 <> 0 + .byte Tile1, Attr1, Y1, X1 +.endif +.if DrawSprite2 <> 0 + .byte Tile2, Attr2, Y1, X2 +.endif +.if DrawSprite3 <> 0 + .byte Tile3, Attr3, Y2, X1 +.endif +.if DrawSprite4 <> 0 + .byte Tile4, Attr4, Y2, X2 +.endif +.endmacro + +.macro MetaspriteBox Object, Animation, Spr1, Spr2, Spr3, Spr4 +.local VramOffset, Bank, Palette, XOffset, YOffset, Mirror + +.if .defined(.ident( .sprintf("%s_%s_VRAM_OFFSET", Object, Animation) )) + VramOffset = .ident( .sprintf("%s_%s_VRAM_OFFSET", Object, Animation) ) +.elseif .defined(.ident( .sprintf("%s_VRAM_OFFSET", Object) )) + VramOffset = .ident( .sprintf("%s_VRAM_OFFSET", Object) ) +.else + VramOffset = 0 +.endif +.if .defined(.ident( .sprintf("%s_%s_BANK", Object, Animation) )) + Bank = .ident( .sprintf("%s_%s_BANK", Object, Animation) ) +.elseif .defined(.ident( .sprintf("%s_BANK", Object) )) + Bank = .ident( .sprintf("%s_BANK", Object) ) +.else + Bank = $ff +.endif +.if .defined(.ident( .sprintf("%s_%s_PALETTE", Object, Animation) )) + Palette = .ident( .sprintf("%s_%s_PALETTE", Object, Animation) ) +.elseif .defined(.ident( .sprintf("%s_PALETTE", Object) )) + Palette = .ident( .sprintf("%s_PALETTE", Object) ) +.else + .error .sprintf("Could not define Metasprite without the palette. Please define either %s_PALETTE or %s_%s_PALETTE", Object, Object, Animation) +.endif + +.if .defined(.ident( .sprintf("%s_%s_Y_OFFSET", Object, Animation) )) + YOffset = <.ident( .sprintf("%s_%s_Y_OFFSET", Object, Animation) ) +.elseif .defined(.ident( .sprintf("%s_Y_OFFSET", Object) )) + YOffset = <.ident( .sprintf("%s_Y_OFFSET", Object) ) +.else + YOffset = 0 +.endif + +.if .defined(.ident( .sprintf("%s_%s_X_OFFSET", Object, Animation) )) + XOffset = <.ident( .sprintf("%s_%s_X_OFFSET", Object, Animation) ) +.elseif .defined(.ident( .sprintf("%s_X_OFFSET", Object) )) + XOffset = <.ident( .sprintf("%s_X_OFFSET", Object) ) +.else + XOffset = 0 +.endif + +.if .defined(.ident( .sprintf("%s_%s_NO_MIRROR", Object, Animation) )) + Mirror = 0 +.elseif .defined(.ident( .sprintf("%s_NO_MIRROR", Object) )) + Mirror = 0 +.else + Mirror = 1 +.endif + +.ident( .sprintf("METASPRITE_%d_BANK", METASPRITES_COUNT) ) = Bank + +.ifdef METASPRITE_BODY + +.if Mirror = 1 +MetaspriteBoxBody Object, Animation, "LEFT", VramOffset, Palette, XOffset, YOffset, Spr1, Spr2, Spr3, Spr4 +.endif +MetaspriteBoxBody Object, Animation, "RIGHT", VramOffset, Palette, XOffset, YOffset, Spr1, Spr2, Spr3, Spr4 + +.if PRINT_METASPRITE_IDS +.out .sprintf("#define METASPRITE_%s_%s 0x%02x", Object, Animation, METASPRITES_COUNT) +.endif +.if Mirror = 1 +.ident( .sprintf("METASPRITE_LEFT_%d_LO", METASPRITES_COUNT) ) = .lobyte(.ident( .sprintf("MetaspriteData_%s_%s_LEFT", Object, Animation) )) +.ident( .sprintf("METASPRITE_LEFT_%d_HI", METASPRITES_COUNT) ) = .hibyte(.ident( .sprintf("MetaspriteData_%s_%s_LEFT", Object, Animation) )) +.else +.ident( .sprintf("METASPRITE_LEFT_%d_LO", METASPRITES_COUNT) ) = .lobyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) +.ident( .sprintf("METASPRITE_LEFT_%d_HI", METASPRITES_COUNT) ) = .hibyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) +.endif + +.ident( .sprintf("METASPRITE_RIGHT_%d_LO", METASPRITES_COUNT) ) = .lobyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) +.ident( .sprintf("METASPRITE_RIGHT_%d_HI", METASPRITES_COUNT) ) = .hibyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) + + +.endif + +MetaspriteReserve .sprintf("%s_%s",Object, Animation) +.endmacro + +.macro MetaspriteDuplicate Name, Mspr +.Local LL, LH, RL, RH, Id, Bank +Id = .ident( .concat("METASPRITE_", Mspr) ) +Bank = .ident( .sprintf("METASPRITE_%d_BANK", Id) ) +.ident( .sprintf("METASPRITE_%d_BANK", METASPRITES_COUNT) ) = Bank + +.ifdef METASPRITE_BODY +.if PRINT_METASPRITE_IDS +.out .sprintf("#define METASPRITE_%s 0x%02x // (duplicate of %s)", Name, METASPRITES_COUNT, Mspr) +.endif + +LL = .ident(.sprintf("METASPRITE_LEFT_%d_LO", Id)) +LH = .ident(.sprintf("METASPRITE_LEFT_%d_HI", Id)) +RL = .ident(.sprintf("METASPRITE_RIGHT_%d_LO", Id)) +RH = .ident(.sprintf("METASPRITE_RIGHT_%d_HI", Id)) + +.ident( .sprintf("METASPRITE_LEFT_%d_LO", METASPRITES_COUNT) ) = LL +.ident( .sprintf("METASPRITE_LEFT_%d_HI", METASPRITES_COUNT) ) = LH +.ident( .sprintf("METASPRITE_RIGHT_%d_LO", METASPRITES_COUNT) ) = RL +.ident( .sprintf("METASPRITE_RIGHT_%d_HI", METASPRITES_COUNT) ) = RH + +.endif + +MetaspriteReserve Name +.endmacro + + +;;;; +; Reserves a spot in the metasprite table for this named constant. +; This is used internally when making a new metasprite, but can also +; be used to make your own metasprite. Just reserve a slot with the name +; of the metasprite and +.macro MetaspriteReserve Name +.ident( .sprintf("METASPRITE_%s", Name) ) = METASPRITES_COUNT +METASPRITES_COUNT .set METASPRITES_COUNT + 1 +.endmacro + +.macro MetaspriteData Name, Left, Right +.Local Id + +Id = .ident(Name) + +.ident( .sprintf("METASPRITE_LEFT_%d_LO", Id) ) = .lobyte(Left) +.ident( .sprintf("METASPRITE_LEFT_%d_HI", Id) ) = .hibyte(Left) +.ifnblank Right +.ident( .sprintf("METASPRITE_RIGHT_%d_LO", Id) ) = .lobyte(Right) +.ident( .sprintf("METASPRITE_RIGHT_%d_HI", Id) ) = .hibyte(Right) +.else +.ident( .sprintf("METASPRITE_RIGHT_%d_LO", Id) ) = .lobyte(Left) +.ident( .sprintf("METASPRITE_RIGHT_%d_HI", Id) ) = .hibyte(Left) +.endif + +.endmacro + + +; .proc DrawAllMetasprites + +; LoopCount = M0 + +; lda #24 - 1 ; size of the different object update list +; sta LoopCount +; lda SpriteShuffleOffset +; clc +; adc #19 +; cmp #24 +; bcc :+ +; ; implicit carry set +; sbc #24 +; : +; sta SpriteShuffleOffset +; lda FrameCounter +; lsr +; lda SpriteShuffleOffset +; bcc ObjectLoopNegative + +; ObjectLoopPositive: +; clc +; adc #13 +; cmp #24 +; bcc :+ +; ; implicit carry set +; sbc #24 +; : +; ; skip index zero since we draw the player first always. +; beq NextLoop +; ; TODO check offscreenbits to make sure they are onscreen still +; tay +; ldx ObjectMetasprite,y +; beq NextLoop +; cpx #METASPRITES_COUNT ; todo remove this after fixing all bugs +; bcs NextLoop +; sta SpriteShuffleOffset +; jsr DrawMetasprite +; lda SpriteShuffleOffset +; NextLoop: +; dec LoopCount +; bpl ObjectLoopPositive +; jmp HandleFloateyNumbers + +; ObjectLoopNegative: +; sec +; sbc #13 +; bcs :+ +; ; implicit carry clear +; adc #24 +; : +; ; skip index zero since we draw the player first always. +; beq NextLoop2 +; ; TODO check offscreenbits to make sure they are onscreen still +; tay +; ldx ObjectMetasprite,y +; beq NextLoop2 +; cpx #METASPRITES_COUNT ; todo remove this after fixing all bugs +; bcs NextLoop2 +; sta SpriteShuffleOffset +; jsr DrawMetasprite +; lda SpriteShuffleOffset +; NextLoop2: +; dec LoopCount +; bpl ObjectLoopNegative + +; HandleFloateyNumbers: +; sta SpriteShuffleOffset + +; ldx #7-1 +; FloateyNumberLoop: +; lda FloateyNum_Control,x ;load control for floatey number +; beq Skip ;if zero, branch to leave +; phx +; jsr FloateyNumberRender +; plx +; Skip: +; dex +; bpl FloateyNumberLoop + + ; lda CurrentOAMOffset + ; pha + + ; ; put the player in slot 0 always + ; lda #0 + ; sta CurrentOAMOffset + + ; ; draw the player first so it doesn't ever flicker + ; ; first clear out the sprites that are reserved for the player + ; lda #$f8 + ; sta Sprite_Y_Position + 0 + ; sta Sprite_Y_Position + 4 + ; sta Sprite_Y_Position + 8 + ; sta Sprite_Y_Position + 12 + +; .if ::MOUSE_DISPLAY_CURSOR +; sta Sprite_Y_Position + 16 +; sta Sprite_Y_Position + 20 +; lda mouse +; bmi connected +; ; not connected so draw the alternate sprite at the middle of the screen +; lda #256 / 2 - 8 +; sta mouse + kMouseX +; lda #240 / 2 - 8 +; sta mouse + kMouseY + +; lda FrameCounter +; and #%00100000 +; beq useothersprite ; switch between connected and not connected +; ldy #1 +; bne loadmetasprite ;unconditional +; useothersprite: +; ldy #0 +; beq loadmetasprite +; connected: +; ldy MouseState +; loadmetasprite: +; ldx MouseStateMetasprite,y +; lda MetaspriteTableLeftLo,x +; sta R0 ; Ptr +; lda MetaspriteTableLeftHi,x +; sta R1 ; Ptr +; lda #0 +; sta R3 ; Atr +; lda mouse + kMouseX +; sta R4 ; Xlo +; lda #0 +; sta R5 ; Xhi +; lda mouse + kMouseY +; sta R6 ; Ylo +; lda #1 +; sta R7 ; Yhi +; jsr MetaspriteRenderLoop +; .endif + + ; ldy #0 + ; ldx ObjectMetasprite,y + ; ; unless the player is currently flickering due to damage taken + ; beq DoneDrawingPlayer + ; ; Load the bank for the player if it changed + ; lda PlayerBankTable,x + ; cmp PlayerChrBank + ; beq :+ + ; sta PlayerChrBank + ; inc ReloadCHRBank + ; : + ; jsr DrawMetasprite + ; DoneDrawingPlayer: + + ; Clear sprites up to the offset + ; Calculate CurrentOAMOffset / 4 * 3 and add that to the OAM clear to jump + ; the middle of the clear routine based on how many we need to clear + ; pla ; CurrentOAMOffset + ; lsr + ; sta M0 + ; lsr + ; ; clc - always cleared + ; adc M0 + ; ; clc - always cleared + ; adc #OAMClear + ; adc #0 + ; sta M1 + ; lda #$f8 + ; jmp (M0) + +; .if ::MOUSE_DISPLAY_CURSOR +; MouseStateMetasprite: +; .byte METASPRITE_MOUSE_POINTER +; .byte METASPRITE_MOUSE_DISCONNECTED +; .byte METASPRITE_MOUSE_PINCH +; .endif +; .endproc + +; MoveAllSpritesOffscreen: +; ldy #0 +; sty CurrentOAMOffset +; lda #$f8 +; OAMClear: +; .repeat 64, I +; sta Sprite_Y_Position + I*4 ; write 248 into OAM data's Y coordinate +; .endrepeat +; rts + + +.proc DrawMetasprite +Ptr = R0 +; OrigOffset = R2 +Atr = R3 +Xlo = R4 +Xhi = R5 +Ylo = R6 +Yhi = R7 +; LoopCount = M0 +; VFlip = M1 + + cpx #METASPRITES_COUNT + bcc + + rts ; Temp work around to prevent crashing when trying to render bad msprs + + + + lda PlayerFacingDir,y + lsr + bne FacingLeft + lda MetaspriteTableRightLo,x + sta Ptr + lda MetaspriteTableRightHi,x + sta Ptr+1 + bne DrawSprite ; unconditional + FacingLeft: + lda MetaspriteTableLeftLo,x + sta Ptr + lda MetaspriteTableLeftHi,x + sta Ptr+1 +DrawSprite: + + ; sty OrigOffset + + lda SprObject_X_Position,y + sec + sbc ScreenLeft_X_Pos + sta Xlo + lda SprObject_PageLoc,y + sbc ScreenLeft_PageLoc + sta Xhi + lda SprObject_Y_HighPos,y + sta Yhi + +; lda #0 +; sta VFlip +; cpy #7 +; bcs SkipVerticalFlipCheck +; lda ObjectVerticalFlip,y +; bpl NoVFlip +; ; Object has a vertical flip attribute set +; sta VFlip +; NoVFlip: +; ; Object has an added Y offset +; ; The negative flag is bit 5 in our number, so sign extend the value +; and #%00111111 +; cmp #%00100000 +; bcc PositiveYOffset +; ; subtracting a small negative offset here +; ora #%11000000 +; clc +; PositiveYOffset: +; adc SprObject_Y_Position,y +; bcc SetYOffset +; ; If the carry is set, then we need to inc Yhi ... TODO +; ; this probably doesn't handle underflow properly? +; inc Yhi +; bcs SetYOffset ; unconditional +; SkipVerticalFlipCheck: + lda SprObject_Y_Position,y +SetYOffset: + sta Ylo + +; lda VFlip +; beq DontSetFlipBit +; lda #OAM_FLIP_V +; .byte $2c ; masks the next lda #0 +; DontSetFlipBit: + lda #0 + ora SprObject_SprAttrib,y + sta Atr + + jsr MetaspriteRenderLoop + + +; lda VFlip +; beq DontShiftPositions +; ldy #0 +; lda (Ptr),y +; cmp #8 + 1 +; bcc DontShiftPositions +; cmp #12 +; beq Flip3 +; ; otherwise flip all 4 sprites +; lda Sprite_Tilenumber-4,x ;with first or second row tiles +; pha ;and save tiles to the stack +; lda Sprite_Tilenumber-8,x +; pha +; lda Sprite_Tilenumber-12,x ;exchange third row tiles +; sta Sprite_Tilenumber-4,x ;with first or second row tiles +; lda Sprite_Tilenumber-16,x +; sta Sprite_Tilenumber-8,x +; pla ;pull first or second row tiles from stack +; sta Sprite_Tilenumber-16,x ;and save in third row +; pla +; sta Sprite_Tilenumber-12,x +; rts +; Flip3: +; ; Custom flip code for bowser's front since he is weird. +; lda Sprite_Y_Position-4,x +; pha +; lda Sprite_Y_Position-12,x +; sta Sprite_Y_Position-8,x +; sta Sprite_Y_Position-4,x +; pla +; sta Sprite_Y_Position-12,x +; DontShiftPositions: + rts + +.endproc + +.proc MetaspriteRenderLoop +Ptr = R0 +Atr = R3 +Xlo = R4 +Xhi = R5 +Ylo = R6 +Yhi = R7 + + ldx CurrentOAMOffset + ldy #0 + lda (Ptr),y + tay + bpl RenderLoop + +; Offscreen sprites end up here + +Skip4: ; X Offscreen + dey +Skip3: ; Y Offscreen + dey +Skip2: + dey + ; Move this sprite offscreen + lda #$f8 + sta Sprite_Y_Position,x + inx + inx + inx + inx + dey + beq LoopEnded +RenderLoop: + ; load the x position and make sure its on screen + clc + lda (Ptr),y + bpl PositiveX + adc Xlo + sta Sprite_X_Position,x + lda Xhi + adc #$ff + beq ContinueAfterX + bne Skip4 + PositiveX: + adc Xlo + sta Sprite_X_Position,x + lda Xhi + adc #0 + bne Skip4 + ContinueAfterX: + dey + + ; load the y position and also make sure its on screen + clc + lda (Ptr),y + bpl PositiveY + ; NegativeY + adc Ylo + sta Sprite_Y_Position,x + lda Yhi + adc #$ff + cmp #1 ; page 1 is the "main" y position + beq ContinueAfterY + bne Skip3 + PositiveY: + adc Ylo + sta Sprite_Y_Position,x + lda Yhi + adc #0 + cmp #1 ; page 1 is the "main" y position + bne Skip3 + ContinueAfterY: + dey + + ; Mix attributes but if the NO_PALETTE bit is set, prevent + ; the palette from changing. + lda (Ptr),y + bit NoPaletteBitMask + beq AllowPaletteChange + ; No palette change bit set, so pull the byte and + ; mask off the palette from the attribute byte + lda Atr + and #%11111100 + ora (Ptr),y + bne WritePalette ; unconditional +AllowPaletteChange: + ora Atr +WritePalette: + sta Sprite_Attributes,x + dey + + ; set the tile number and move to the next sprite + lda (Ptr),y + sta Sprite_Tilenumber,x + inx + inx + inx + inx + dey + bne RenderLoop +LoopEnded: + stx CurrentOAMOffset + rts + +NoPaletteBitMask: + .byte (SPR_NO_PALETTE >> 8) +.endproc + +; ;------------------------------------------------------------------------------------- +; .proc FloateyNumberRender +; Ptr = R0 +; ; OrigOffset = R2 +; Atr = R3 +; Xlo = R4 +; Xhi = R5 +; Ylo = R6 +; Yhi = R7 + +; lda FloateyNum_Y_Pos,x ;get vertical coordinate for +; cmp #$18 ;floatey number, if coordinate in the +; bcc SetupNumSpr ;status bar, branch +; sbc #$01 +; sta FloateyNum_Y_Pos,x ;otherwise subtract one and store as new +; SetupNumSpr: +; sta Ylo +; lda #1 +; sta Yhi +; lda #0 +; sta Atr +; sta Xhi +; lda FloateyNum_X_Pos,x +; sta Xlo +; ldy FloateyNum_Control,x +; ldx FloateyNumMetasprites-1,y +; lda MetaspriteTableRightLo,x +; sta Ptr +; lda MetaspriteTableRightHi,x +; sta Ptr+1 +; jmp MetaspriteRenderLoop +; ; implicit rts + +; FloateyNumMetasprites: +; .byte METASPRITE_NUMBER_100 +; .byte METASPRITE_NUMBER_200 +; .byte METASPRITE_NUMBER_400 +; .byte METASPRITE_NUMBER_500 +; .byte METASPRITE_NUMBER_800 +; .byte METASPRITE_NUMBER_1000 +; .byte METASPRITE_NUMBER_2000 +; .byte METASPRITE_NUMBER_4000 +; .byte METASPRITE_NUMBER_5000 +; .byte METASPRITE_NUMBER_8000 +; .byte METASPRITE_NUMBER_1UP + +; .endproc diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s new file mode 100644 index 000000000..2e9a639b2 --- /dev/null +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -0,0 +1,600 @@ + +.segment "PRG6" + +;------------------------------------------------------------------------------------- +.reloc +; .proc SFXSoundEngine + ;jsr $92F4 + ;jsr $9408 + ;jsr $95A7 + ; We need to clock the counters each frame to keep audio synced + ; zelda 2 already clocks the framecounter +; lda #$ff +; sta SND_FRAMECOUNTER ;disable irqs and set frame counter mode??? +; SFXSoundEngine: +; lda PauseModeFlag ;is sound already in pause mode? +; bne InPause +; lda PauseSoundQueue ;if not, check pause sfx queue +; cmp #$01 +; bne RunSoundSubroutines ;if queue is empty, skip pause mode routine +; InPause: lda PauseSoundBuffer ;check pause sfx buffer +; bne ContPau +; lda PauseSoundQueue ;check pause queue +; beq SkipSoundSubroutines +; ; Play the ding dong sound for pausing +; sta PauseSoundBuffer ;if queue full, store in buffer and activate +; sta Square1SoundBuffer +; ; sta PauseModeFlag ;pause mode to interrupt game sounds +; lda #$00 ;disable sound and clear sfx buffers +; ; sta SND_MASTERCTRL_REG +; sta Square2SoundBuffer +; sta NoiseSoundBuffer +; ; lda #$0f +; ; sta SND_MASTERCTRL_REG ;enable sound again +; lda #$2a ;store length of sound in pause counter +; sta Squ1_SfxLenCounter +; PTone1F: lda #$44 ;play first tone +; bne PTRegC ;unconditional branch +; ContPau: lda Squ1_SfxLenCounter ;check pause length left +; cmp #$24 ;time to play second? +; beq PTone2F +; cmp #$1e ;time to play first again? +; beq PTone1F +; cmp #$18 ;time to play second again? +; bne DecPauC ;only load regs during times, otherwise skip +; PTone2F: lda #$64 ;store reg contents and play the pause sfx +; PTRegC: ldx #$84 +; ldy #$7f +; jsr PlaySqu1Sfx +; DecPauC: dec Squ1_SfxLenCounter ;decrement pause sfx counter +; bne SkipSoundSubroutines + ; lda #$00 ;disable sound if in pause mode and + ; sta SND_MASTERCTRL_REG ;not currently playing the pause sfx + ; lda PauseSoundBuffer ;if no longer playing pause sfx, check to see + ; cmp #$02 ;if we need to be playing sound again + ; bne SkipPIn +; lda #$00 ;clear pause mode to allow game sounds again +; sta PauseModeFlag +; SkipPIn: lda #$00 ;clear pause sfx buffer +; sta PauseSoundBuffer +; beq SkipSoundSubroutines + +; RunSoundSubroutines: +; jsr Square1SfxHandler ;play sfx on square channel 1 +; jsr Square2SfxHandler ; '' '' '' square channel 2 +; jsr NoiseSfxHandler ; '' '' '' noise channel +; ; jsr MusicHandler ;play music on all channels +; ; lda #$00 ;clear the music queues +; ; sta AreaMusicQueue +; ; sta EventMusicQueue + +; SkipSoundSubroutines: +; lda #$00 ;clear the sound effects queues +; sta Square1SoundQueue +; sta Square2SoundQueue +; sta NoiseSoundQueue +; ; sta PauseSoundQueue +; ; ldy DAC_Counter ;load some sort of counter +; ; lda AreaMusicBuffer +; ; and #%00000011 ;check for specific music +; ; beq NoIncDAC +; ; inc DAC_Counter ;increment and check counter +; ; cpy #$30 +; ; bcc StrWave ;if not there yet, just store it +; ; NoIncDAC: tya +; ; beq StrWave ;if we are at zero, do not decrement +; ; dec DAC_Counter ;decrement counter +; ; StrWave: sty SND_DELTA_REG+1 ;store into DMC load register (??) +; rts ;we are done here +; ; .endproc + +;-------------------------------- + +Dump_Squ1_Regs: + sty SND_SQUARE1_REG+1 ;dump the contents of X and Y into square 1's control regs + stx SND_SQUARE1_REG + rts + +PlaySqu1Sfx: + jsr Dump_Squ1_Regs ;do sub to set ctrl regs for square 1, then set frequency regs + +SetFreq_Squ1: + ldx #$00 ;set frequency reg offset for square 1 sound channel + +Dump_Freq_Regs: + tay + lda FreqRegLookupTbl+1,y ;use previous contents of A for sound reg offset + beq NoTone ;if zero, then do not load + sta SND_SQUARE1_REG+2,x ;first byte goes into LSB of frequency divider + lda FreqRegLookupTbl,y ;second byte goes into 3 MSB plus extra bit for + ora #%00001000 ;length counter + sta SND_SQUARE1_REG+3,x +NoTone: rts + +Dump_Sq2_Regs: + stx SND_SQUARE2_REG ;dump the contents of X and Y into square 2's control regs + sty SND_SQUARE2_REG+1 + rts + +PlaySqu2Sfx: + jsr Dump_Sq2_Regs ;do sub to set ctrl regs for square 2, then set frequency regs + +SetFreq_Squ2: + ldx #$04 ;set frequency reg offset for square 2 sound channel + bne Dump_Freq_Regs ;unconditional branch + +;-------------------------------- + +SwimStompEnvelopeData: + .byte $9f, $9b, $98, $96, $95, $94, $92, $90 + .byte $90, $9a, $97, $95, $93, $92 + +PlayFlagpoleSlide: + lda #$40 ;store length of flagpole sound + sta Squ1_SfxLenCounter + lda #$62 ;load part of reg contents for flagpole sound + jsr SetFreq_Squ1 + ldx #$99 ;now load the rest + bne FPS2nd + +PlaySmallJump: + lda #$26 ;branch here for small mario jumping sound + bne JumpRegContents + +PlayBigJump: + lda #$18 ;branch here for big mario jumping sound + +JumpRegContents: + ldx #$82 ;note that small and big jump borrow each others' reg contents + ldy #$a7 ;anyway, this loads the first part of mario's jumping sound + jsr PlaySqu1Sfx + lda #$28 ;store length of sfx for both jumping sounds + sta Squ1_SfxLenCounter ;then continue on here + +ContinueSndJump: + lda Squ1_SfxLenCounter ;jumping sounds seem to be composed of three parts + cmp #$25 ;check for time to play second part yet + bne N2Prt + ldx #$5f ;load second part + ldy #$f6 + bne DmpJpFPS ;unconditional branch +N2Prt: cmp #$20 ;check for third part + bne DecJpFPS + ldx #$48 ;load third part +FPS2nd: ldy #$bc ;the flagpole slide sound shares part of third part +DmpJpFPS: jsr Dump_Squ1_Regs + bne DecJpFPS ;unconditional branch outta here + +PlayFireballThrow: + lda #$05 + ldy #$99 ;load reg contents for fireball throw sound + bne Fthrow ;unconditional branch + +PlayBump: + lda #$0a ;load length of sfx and reg contents for bump sound + ldy #$93 +Fthrow: ldx #$9e ;the fireball sound shares reg contents with the bump sound + sta Squ1_SfxLenCounter + lda #$0c ;load offset for bump sound + jsr PlaySqu1Sfx + +ContinueBumpThrow: + lda Squ1_SfxLenCounter ;check for second part of bump sound + cmp #$06 + bne DecJpFPS + lda #$bb ;load second part directly + sta SND_SQUARE1_REG+1 +DecJpFPS: bne BranchToDecLength1 ;unconditional branch + + +Square1SfxHandler: + ldy Square1SoundQueue ;check for sfx in queue + beq CheckSfx1Buffer + sty Square1SoundBuffer ;if found, put in buffer + bmi PlaySmallJump ;small jump + lsr Square1SoundQueue + bcs PlayBigJump ;big jump + lsr Square1SoundQueue + bcs PlayBump ;bump + lsr Square1SoundQueue + bcs PlaySwimStomp ;swim/stomp + lsr Square1SoundQueue + bcs PlaySmackEnemy ;smack enemy + lsr Square1SoundQueue + bcc + + jmp PlayPipeDownInj ;pipedown/injury + + + lsr Square1SoundQueue + bcs PlayFireballThrow ;fireball throw + lsr Square1SoundQueue + bcc + + jmp PlayFlagpoleSlide ;slide flagpole + + + +CheckSfx1Buffer: + lda Square1SoundBuffer ;check for sfx in buffer + beq ExS1H ;if not found, exit sub + bmi ContinueSndJump ;small mario jump + lsr + bcs ContinueSndJump ;big mario jump + lsr + bcs ContinueBumpThrow ;bump + lsr + bcs ContinueSwimStomp ;swim/stomp + lsr + bcs ContinueSmackEnemy ;smack enemy + lsr + bcs ContinuePipeDownInj ;pipedown/injury + lsr + bcs ContinueBumpThrow ;fireball throw + lsr + bcs DecrementSfx1Length ;slide flagpole +ExS1H: rts + +PlaySwimStomp: + lda #$0e ;store length of swim/stomp sound + sta Squ1_SfxLenCounter + ldy #$9c ;store reg contents for swim/stomp sound + ldx #$9e + lda #$26 + jsr PlaySqu1Sfx + +ContinueSwimStomp: + ldy Squ1_SfxLenCounter ;look up reg contents in data section based on + lda SwimStompEnvelopeData-1,y ;length of sound left, used to control sound's + sta SND_SQUARE1_REG ;envelope + cpy #$06 + bne BranchToDecLength1 + lda #$9e ;when the length counts down to a certain point, put this + sta SND_SQUARE1_REG+2 ;directly into the LSB of square 1's frequency divider + +BranchToDecLength1: + bne DecrementSfx1Length ;unconditional branch (regardless of how we got here) + +PlaySmackEnemy: + lda #$0e ;store length of smack enemy sound + ldy #$cb + ldx #$9f + sta Squ1_SfxLenCounter + lda #$28 ;store reg contents for smack enemy sound + jsr PlaySqu1Sfx + bne DecrementSfx1Length ;unconditional branch + +ContinueSmackEnemy: + ldy Squ1_SfxLenCounter ;check about halfway through + cpy #$08 + bne SmSpc + lda #$a0 ;if we're at the about-halfway point, make the second tone + sta SND_SQUARE1_REG+2 ;in the smack enemy sound + lda #$9f + bne SmTick +SmSpc: lda #$90 ;this creates spaces in the sound, giving it its distinct noise +SmTick: sta SND_SQUARE1_REG + +DecrementSfx1Length: + dec Squ1_SfxLenCounter ;decrement length of sfx + bne ExSfx1 + +StopSquare1Sfx: + ldx #$00 ;if end of sfx reached, clear buffer + stx Square1SoundBuffer ;and stop making the sfx + ; ldx #$0e + ; stx SND_MASTERCTRL_REG + ; ldx #$0f + ; stx SND_MASTERCTRL_REG +ExSfx1: rts + +PlayPipeDownInj: + lda #$2f ;load length of pipedown sound + sta Squ1_SfxLenCounter + +ContinuePipeDownInj: + lda Squ1_SfxLenCounter ;some bitwise logic, forces the regs + lsr ;to be written to only during six specific times + bcs NoPDwnL ;during which d3 must be set and d1-0 must be clear + lsr + bcs NoPDwnL + and #%00000010 + beq NoPDwnL + ldy #$91 ;and this is where it actually gets written in + ldx #$9a + lda #$44 + jsr PlaySqu1Sfx +NoPDwnL: jmp DecrementSfx1Length + +;-------------------------------- + +ExtraLifeFreqData: + .byte $58, $02, $54, $56, $4e, $44 + +PowerUpGrabFreqData: + .byte $4c, $52, $4c, $48, $3e, $36, $3e, $36, $30 + .byte $28, $4a, $50, $4a, $64, $3c, $32, $3c, $32 + .byte $2c, $24, $3a, $64, $3a, $34, $2c, $22, $2c + +PUp_VGrow_FreqData: + .byte $14, $04, $22, $24, $16, $04, $24, $26 ;used by both + .byte $18, $04, $26, $28, $1a, $04, $28, $2a + .byte $1c, $04, $2a, $2c, $1e, $04, $2c, $2e ;used by vinegrow + .byte $20, $04, $2e, $30, $22, $04, $30, $32 + +PlayCoinGrab: + lda #$35 ;load length of coin grab sound + ldx #$8d ;and part of reg contents + bne CGrab_TTickRegL + +PlayTimerTick: + lda #$06 ;load length of timer tick sound + ldx #$98 ;and part of reg contents + +CGrab_TTickRegL: + sta Squ2_SfxLenCounter + ldy #$7f ;load the rest of reg contents + lda #$42 ;of coin grab and timer tick sound + jsr PlaySqu2Sfx + +ContinueCGrabTTick: + lda Squ2_SfxLenCounter ;check for time to play second tone yet + cmp #$30 ;timer tick sound also executes this, not sure why + bne N2Tone + lda #$54 ;if so, load the tone directly into the reg + sta SND_SQUARE2_REG+2 +N2Tone: bne DecrementSfx2Length + +PlayBlast: + lda #$20 ;load length of fireworks/gunfire sound + sta Squ2_SfxLenCounter + ldy #$94 ;load reg contents of fireworks/gunfire sound + lda #$5e + bne SBlasJ + +ContinueBlast: + lda Squ2_SfxLenCounter ;check for time to play second part + cmp #$18 + bne DecrementSfx2Length + ldy #$93 ;load second part reg contents then + lda #$18 +SBlasJ: beq + + jmp BlstSJp ;unconditional branch to load rest of reg contents + + + +PlayPowerUpGrab: + lda #$36 ;load length of power-up grab sound + sta Squ2_SfxLenCounter + +ContinuePowerUpGrab: + lda Squ2_SfxLenCounter ;load frequency reg based on length left over + lsr ;divide by 2 + bcs DecrementSfx2Length ;alter frequency every other frame + tay + lda PowerUpGrabFreqData-1,y ;use length left over / 2 for frequency offset + ldx #$5d ;store reg contents of power-up grab sound + ldy #$7f + +LoadSqu2Regs: + jsr PlaySqu2Sfx + +DecrementSfx2Length: + dec Squ2_SfxLenCounter ;decrement length of sfx + bne ExSfx2 + +EmptySfx2Buffer: + ldx #$00 ;initialize square 2's sound effects buffer + stx Square2SoundBuffer + +StopSquare2Sfx: + ; ldx #$0d ;stop playing the sfx + ; stx SND_MASTERCTRL_REG + ; ldx #$0f + ; stx SND_MASTERCTRL_REG +ExSfx2: rts + +Square2SfxHandler: + lda Square2SoundBuffer ;special handling for the 1-up sound to keep it + and #Sfx_ExtraLife ;from being interrupted by other sounds on square 2 + bne ContinueExtraLife + ldy Square2SoundQueue ;check for sfx in queue + beq CheckSfx2Buffer + sty Square2SoundBuffer ;if found, put in buffer and check for the following + bmi PlayBowserFall ;bowser fall + lsr Square2SoundQueue + bcs PlayCoinGrab ;coin grab + lsr Square2SoundQueue + bcs PlayGrowPowerUp ;power-up reveal + lsr Square2SoundQueue + bcs PlayGrowVine ;vine grow + lsr Square2SoundQueue + bcs PlayBlast ;fireworks/gunfire + lsr Square2SoundQueue + bcc + + jmp PlayTimerTick ;timer tick + + + lsr Square2SoundQueue + bcs PlayPowerUpGrab ;power-up grab + lsr Square2SoundQueue + bcs PlayExtraLife ;1-up + +CheckSfx2Buffer: + lda Square2SoundBuffer ;check for sfx in buffer + beq ExS2H ;if not found, exit sub + bmi ContinueBowserFall ;bowser fall + lsr + bcs Cont_CGrab_TTick ;coin grab + lsr + bcs ContinueGrowItems ;power-up reveal + lsr + bcs ContinueGrowItems ;vine grow + lsr + bcc + + jmp ContinueBlast ;fireworks/gunfire + + + lsr + bcs Cont_CGrab_TTick ;timer tick + lsr + bcs ContinuePowerUpGrab ;power-up grab + lsr + bcs ContinueExtraLife ;1-up +ExS2H: rts + +Cont_CGrab_TTick: + jmp ContinueCGrabTTick + +JumpToDecLength2: + jmp DecrementSfx2Length + +PlayBowserFall: + lda #$38 ;load length of bowser defeat sound + sta Squ2_SfxLenCounter + ldy #$c4 ;load contents of reg for bowser defeat sound + lda #$18 +BlstSJp: bne PBFRegs + +ContinueBowserFall: + lda Squ2_SfxLenCounter ;check for almost near the end + cmp #$08 + beq + + jmp DecrementSfx2Length + + + ldy #$a4 ;if so, load the rest of reg contents for bowser defeat sound + lda #$5a +PBFRegs: ldx #$9f ;the fireworks/gunfire sound shares part of reg contents here +EL_LRegs: beq + + jmp LoadSqu2Regs ;this is an unconditional branch outta here + + + +PlayExtraLife: + lda #$30 ;load length of 1-up sound + sta Squ2_SfxLenCounter + +ContinueExtraLife: + lda Squ2_SfxLenCounter + ldx #$03 ;load new tones only every eight frames +DivLLoop: lsr + bcs JumpToDecLength2 ;if any bits set here, branch to dec the length + dex + bne DivLLoop ;do this until all bits checked, if none set, continue + tay + lda ExtraLifeFreqData-1,y ;load our reg contents + ldx #$82 + ldy #$7f + bne EL_LRegs ;unconditional branch + +PlayGrowPowerUp: + lda #$10 ;load length of power-up reveal sound + bne GrowItemRegs + +PlayGrowVine: + lda #$20 ;load length of vine grow sound + +GrowItemRegs: + sta Squ2_SfxLenCounter + lda #$7f ;load contents of reg for both sounds directly + sta SND_SQUARE2_REG+1 + lda #$00 ;start secondary counter for both sounds + sta Sfx_SecondaryCounter + +ContinueGrowItems: + inc Sfx_SecondaryCounter ;increment secondary counter for both sounds + lda Sfx_SecondaryCounter ;this sound doesn't decrement the usual counter + lsr ;divide by 2 to get the offset + tay + cpy Squ2_SfxLenCounter ;have we reached the end yet? + beq StopGrowItems ;if so, branch to jump, and stop playing sounds + lda #$9d ;load contents of other reg directly + sta SND_SQUARE2_REG + lda PUp_VGrow_FreqData,y ;use secondary counter / 2 as offset for frequency regs + jmp SetFreq_Squ2 + rts ; TODO check this RTS can be removed + +StopGrowItems: + jmp EmptySfx2Buffer ;branch to stop playing sounds + +;-------------------------------- + +BrickShatterFreqData: + .byte $01, $0e, $0e, $0d, $0b, $06, $0c, $0f + .byte $0a, $09, $03, $0d, $08, $0d, $06, $0c + +PlayBrickShatter: + lda #$20 ;load length of brick shatter sound + sta Noise_SfxLenCounter + +ContinueBrickShatter: + lda Noise_SfxLenCounter + lsr ;divide by 2 and check for bit set to use offset + bcc DecrementSfx3Length + tay + ldx BrickShatterFreqData,y ;load reg contents of brick shatter sound + lda BrickShatterEnvData,y + +PlayNoiseSfx: + sta SND_NOISE_REG ;play the sfx + stx SND_NOISE_REG+2 + lda #$18 + sta SND_NOISE_REG+3 + +DecrementSfx3Length: + dec Noise_SfxLenCounter ;decrement length of sfx + bne ExSfx3 + lda #$f0 ;if done, stop playing the sfx + sta SND_NOISE_REG + lda #$00 + sta NoiseSoundBuffer +ExSfx3: rts + +NoiseSfxHandler: + ldy NoiseSoundQueue ;check for sfx in queue + beq CheckNoiseBuffer + sty NoiseSoundBuffer ;if found, put in buffer + lsr NoiseSoundQueue + bcs PlayBrickShatter ;brick shatter + lsr NoiseSoundQueue + bcs PlayBowserFlame ;bowser flame + +CheckNoiseBuffer: + lda NoiseSoundBuffer ;check for sfx in buffer + beq ExNH ;if not found, exit sub + lsr + bcs ContinueBrickShatter ;brick shatter + lsr + bcs ContinueBowserFlame ;bowser flame +ExNH: rts + +PlayBowserFlame: + lda #$40 ;load length of bowser flame sound + sta Noise_SfxLenCounter + +ContinueBowserFlame: + lda Noise_SfxLenCounter + lsr + tay + ldx #$0f ;load reg contents of bowser flame sound + lda BowserFlameEnvData-1,y + bne PlayNoiseSfx ;unconditional branch here + + + +BowserFlameEnvData: + .byte $15, $16, $16, $17, $17, $18, $19, $19 + .byte $1a, $1a, $1c, $1d, $1d, $1e, $1e, $1f + .byte $1f, $1f, $1f, $1e, $1d, $1c, $1e, $1f + .byte $1f, $1e, $1d, $1c, $1a, $18, $16, $14 + +BrickShatterEnvData: + .byte $15, $16, $16, $17, $17, $18, $19, $19 + .byte $1a, $1a, $1c, $1d, $1d, $1e, $1e, $1f + +FreqRegLookupTbl: + .byte $00, $88, $00, $2f, $00, $00 + .byte $02, $a6, $02, $80, $02, $5c, $02, $3a + .byte $02, $1a, $01, $df, $01, $c4, $01, $ab + .byte $01, $93, $01, $7c, $01, $67, $01, $53 + .byte $01, $40, $01, $2e, $01, $1d, $01, $0d + .byte $00, $fe, $00, $ef, $00, $e2, $00, $d5 + .byte $00, $c9, $00, $be, $00, $b3, $00, $a9 + .byte $00, $a0, $00, $97, $00, $8e, $00, $86 + .byte $00, $77, $00, $7e, $00, $71, $00, $54 + .byte $00, $64, $00, $5f, $00, $59, $00, $50 + .byte $00, $47, $00, $43, $00, $3b, $00, $35 + .byte $00, $2a, $00, $23, $04, $75, $03, $57 + .byte $02, $f9, $02, $cf, $01, $fc, $00, $6a diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 35a850342..353bfb04b 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -1,3 +1,6 @@ +.ifndef _Z2R_INC +_Z2R_INC = 1 + .macpack common ; janky macro res till it starts working @@ -111,6 +114,22 @@ NewSideviewInit = $8cec ClearNametables = $d266 JumpEngine = $d385 +; These are new variables that use free memory +Z2Noise_SfxLenCounter = $07f5 +Z2Squ2_SfxLenCounter = $07f7 +Z2Squ1_SfxLenCounter = $07f9 +Z2NoiseSoundBuffer = $07fd +Z2Square2SoundBuffer = $07fe +Z2Square1SoundBuffer = $07ff + +Z2NoiseSoundQueue = $ed +Z2Square2SoundQueue = $ee +Z2Square1SoundQueue = $ef + +NoiseSoundQueue = $f0 +Square2SoundQueue = $f1 +Square1SoundQueue = $f2 + ;; Collectable items ; Hardcoded list of item ids for the new global item table. ; These should match the constants from the Collectables Enum @@ -156,6 +175,7 @@ ITEM_SPELL_SPELL = $22 ITEM_THUNDER_SPELL = $23 ITEM_DASH_SPELL = $24 + ;; NEW VARIABLE ALLOCATIONS ; Stack RAM StackRAMBase = $0120 @@ -177,6 +197,228 @@ RESV IrqSetupAlready .assert RES_BASE + RES_OFFSET < $01ad +.if ENABLE_Z2_MARIO + +; New RAM values that aren't from vanilla SMB1 code +RESV mapMarioIndex +RESV mapMarioTimer +RESV mapMarioPrevDirection + +;;; +; MARIO related values that are not directly mapped to vanilla z2 object values + +RESV FireballCounter +; RESV ObjectOffset +RESV FBall_OffscreenBits +RESV AltEntranceControl +RESV PlayerEntranceCtrl +RESV JoypadOverride +RESV Vine_Height +RESV Player_State +RESV DisableCollisionDet +RESV GameEngineSubroutine +RESV AreaType +; RESV A_B_Buttons +A_B_Buttons = $741 +RESV PreviousA_B_Buttons +; RESV Left_Right_Buttons +Left_Right_Buttons = $742 +; RESV Up_Down_Buttons +Up_Down_Buttons = $743 +RESV PlayerSize +RESV DeathMusicLoaded +RESV EventMusicQueue +RESV EventMusicBuffer ; TODO use the vanilla music queues +RESV WarpZoneControl +; RESV Player_CollisionBits +RESV HalfwayPage +RESV SwimmingFlag +RESV CurrentOAMOffset +; RESV ObjectMetasprite +RESV PlayerAnimCtrl +RESV PlayerChangeSizeFlag +RESV VerticalForceDown +RESV VerticalForce +RESV JumpOrigin_Y_Position +RESV JumpOrigin_Y_HighPos +RESV DiffToHaltJump +RESV PlayerAnimTimerSet +RESV JumpspringAnimCtrl +RESV Player_XSpeedAbsolute +; RESV Whirlpool_Flag +RESV RunningSpeed +RESV MaximumLeftSpeed +RESV MaximumRightSpeed +RESV FrictionAdderLow +RESV FrictionAdderHigh +RESV Player_Pos_ForScroll +RESV Player_X_Scroll +RESV Player_X_MoveForce +RESV PlayerStandingMetatile ; added to keep this for door checks and other metatiles + +; Non-timer timer. +RESV FireballThrowingTimer + +RESV TimerControl +RESV IntervalTimerControl + +Timers = PlayerAnimTimer +RESV PlayerAnimTimer +RESV ClimbSideTimer +RESV JumpSwimTimer +RESV RunningTimer +RESV SideCollisionTimer +RESV BlockBounceTimer +RESV ChangeAreaTimer +FRAME_TIMER_COUNT = ChangeAreaTimer - Timers +RESV StarInvincibleTimer +ALL_TIMER_COUNT = StarInvincibleTimer - Timers + +RESV SpriteShuffleOffset +RESV CurrentCHRBank +RESV PlayerChrBank +RESV ReloadCHRBank +RESV ObjectVerticalFlip +RESV Local_eb + +; Values use for SMB1 sfx audio engine + +RESV Sfx_SecondaryCounter +RESV PauseSoundBuffer +RESV PauseModeFlag +RESV PauseSoundQueue +RESV Square1SoundBuffer +RESV Square2SoundBuffer +RESV NoiseSoundBuffer +RESV Squ1_SfxLenCounter +RESV Squ2_SfxLenCounter +RESV Noise_SfxLenCounter + +; MARIO SPECIFIC RAM LOCATIONS +; These are values that are hardcoded to match where Zelda 2 places them +Player_X_Speed = $70 +SprObject_X_Speed = Player_X_Speed +Player_X_Position = $4D +SprObject_X_Position = Player_X_Position +Player_PageLoc = $3B +SprObject_PageLoc = Player_PageLoc +Player_Y_Position = $29 +SprObject_Y_Position = Player_Y_Position +Player_Y_HighPos = $19 +SprObject_Y_HighPos = Player_Y_HighPos + +Player_CollisionBits = $A7 +ScrollAmount = $14 +ObjectMetasprite = $80 + +ProjectileOffset = 13 +FireballOffset = 13 +FireballMetasprite = ObjectMetasprite + FireballOffset +Fireball_X_Position = SprObject_X_Position + FireballOffset +Fireball_Y_Position = SprObject_Y_Position + FireballOffset +Fireball_Y_HighPos = SprObject_Y_HighPos + FireballOffset +Fireball_PageLoc = SprObject_PageLoc + FireballOffset +Fireball_X_Speed = SprObject_X_Speed + FireballOffset +Fireball_Y_Speed = SprObject_Y_Speed + FireballOffset +RESV FireballBouncingFlag, 2 +RESV Fireball_State, 2 + +; For reasons I cannot explain, player_x_moveforce is not the same as sprobject_x_moveforce +SprObject_X_MoveForce = $03d6 ; Player_X_MoveForce +Player_Y_MoveForce = $03e6 +SprObject_Y_MoveForce = Player_Y_MoveForce + +Player_Y_Speed = $057D +SprObject_Y_Speed = Player_Y_Speed + +FrameCounter = $12 +; CrouchingFlag = $17 +RESV CrouchingFlag +Player_MovingDir = $5F +PlayerFacingDir = $9F +SavedJoypadBits = $F7 +ScreenLeft_PageLoc = $72a +ScreenRight_PageLoc = $72b +ScreenEdge_PageLoc = ScreenLeft_PageLoc +ScreenLeft_X_Pos = $72c +ScreenRight_X_Pos = $72d +ScreenEdge_X_Pos = ScreenLeft_X_Pos + +InjuryTimer = $510 +NumberofLives = $0700 +; Player_SprAttrib = $0752 +; SprObject_SprAttrib = Player_SprAttrib +ScrollLock = $0728 + +MoveObjectHorizontally = $D1CE +CheckForSolidMTiles = $850C +CheckSideviewTransition = $E14A + +VRAM_Buffer1_Offset = $302 +VRAM_Buffer1 = $303 + +Sprite_Y_Position = $200 +Sprite_Tilenumber = $201 +Sprite_Attributes = $202 +Sprite_X_Position = $203 +Sprite_Data = Sprite_Y_Position + +;; Constants + +CHR_BIGMARIO = $6c +CHR_MARIOACTION = $6d +CHR_SMALLFIRE = $6e +CHR_SMALLMARIO = $6f + +A_Button = %10000000 +B_Button = %01000000 +Select_Button = %00100000 +Start_Button = %00010000 +Up_Dir = %00001000 +Down_Dir = %00000100 +Left_Dir = %00000010 +Right_Dir = %00000001 + +;sound effects constants +Sfx_SmallJump = %10000000 +Sfx_Flagpole = %01000000 +Sfx_Fireball = %00100000 +Sfx_PipeDown_Injury = %00010000 +Sfx_EnemySmack = %00001000 +Sfx_EnemyStomp = %00000100 +Sfx_Bump = %00000010 +Sfx_BigJump = %00000001 + +Sfx_BowserFall = %10000000 +Sfx_ExtraLife = %01000000 +Sfx_PowerUpGrab = %00100000 +Sfx_TimerTick = %00010000 +Sfx_Blast = %00001000 +Sfx_GrowVine = %00000100 +Sfx_GrowPowerUp = %00000010 +Sfx_CoinGrab = %00000001 + +;music constants +Silence = %10000000 + +StarPowerMusic = %01000000 +PipeIntroMusic = %00100000 +CloudMusic = %00010000 +CastleMusic = %00001000 +UndergroundMusic = %00000100 +WaterMusic = %00000010 +GroundMusic = %00000001 + +TimeRunningOutMusic = %01000000 +EndOfLevelMusic = %00100000 +AltGameOverMusic = %00010000 +EndOfCastleMusic = %00001000 +VictoryMusic = %00000100 +GameOverMusic = %00000010 +DeathMusic = %00000001 + +.endif ; ENABLE_Z2_MARIO + ;; Stat tracking variables in SRAM ; This value is the second header byte for file 1 thats unused. @@ -268,4 +510,57 @@ STAT_TRACKING_SIZE = RES_OFFSET ; or load regular sideviews from the usual location RESV SideViewBuffer, $0100 +ClearSRAMStart = RES_OFFSET + +.if ENABLE_Z2_MARIO +; There isn't a general purpose "attribute" mix so we can make our own +RESV SprObject_SprAttrib, 16 +Player_SprAttrib = SprObject_SprAttrib +Fireball_SprAttrib = SprObject_SprAttrib + FireballOffset + +RESV Player_YMoveForceFractional, 16 + +ClearSRAMEnd = RES_OFFSET + .assert RES_OFFSET + StatTrackingBase < $6957 + +;;; Update a handful of refs to point to the given address. +;;; Usage: +;;; UPDATE_BYTE byte @ refs... +;;; Where |refs| is a space-separated list of addresses, and +;;; |byte| is a single literal to insert into each ref. +.define UPDATE_BYTE {target @ ref refs .eol} \ +.pushseg .eol \ +.if $00000 <= ref - $10 .and ref - $10 < $04000 .eol \ + .segment "PRG0" .eol \ + .org ((ref - $10) .mod $4000) + $8000 .eol \ +.elseif $04000 <= ref - $10 .and ref - $10 < $08000 .eol \ + .segment "PRG1" .eol \ + .org ((ref - $10) .mod $4000) + $8000 .eol \ +.elseif $08000 <= ref - $10 .and ref - $10 < $0c000 .eol \ + .segment "PRG2" .eol \ + .org ((ref - $10) .mod $4000) + $8000 .eol \ +.elseif $0c000 <= ref - $10 .and ref - $10 < $10000 .eol \ + .segment "PRG3" .eol \ + .org ((ref - $10) .mod $4000) + $8000 .eol \ +.elseif $10000 <= ref - $10 .and ref - $10 < $14000 .eol \ + .segment "PRG4" .eol \ + .org ((ref - $10) .mod $4000) + $8000 .eol \ +.elseif $14000 <= ref - $10 .and ref - $10 < $18000 .eol \ + .segment "PRG5" .eol \ + .org ((ref - $10) .mod $4000) + $8000 .eol \ +.elseif $18000 <= ref - $10 .and ref - $10 < $1c000 .eol \ + .segment "PRG6" .eol \ + .org ((ref - $10) .mod $4000) + $8000 .eol \ +.elseif $1c000 <= ref - $10 .and ref - $10 < $20000 .eol \ + .segment "PRG7" .eol \ + .org ((ref - $10) .mod $4000) + $c000 .eol \ +.else .eol \ + .error "bad ref" .eol \ +.endif .eol \ + .byte target .eol \ +.popseg .eol \ +UPDATE_BYTE target @ refs +.define UPDATE_BYTE {target @ .eol} + +.endif From 5178b21c5bdcee150502f5b9eda62d6efcc619fe Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sun, 24 Aug 2025 09:41:20 -0400 Subject: [PATCH 003/128] WIP - Bringing files over from z2mario --- RandomizerCore/Asm/z2mario/integration.s | 16 ++++++++-------- RandomizerCore/Asm/z2r.inc | 4 +++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index e351fc42b..2dd833d9a 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -254,10 +254,10 @@ ClearCollisionRAM: ldy #$d0 lda #0 @loop: - sta $6340-1,y - sta $6340-1+$d0,y - sta $6340-1+$d0*2,y - sta $6340-1+$d0*3,y + sta COLLISION_TILES-1,y + sta COLLISION_TILES-1+$d0,y + sta COLLISION_TILES-1+$d0*2,y + sta COLLISION_TILES-1+$d0*3,y dey bne @loop rts @@ -272,22 +272,22 @@ UpdateCollisionFromMap: lda $6000-1,x jsr CheckForSolidMTiles bcc @skip1 - sta $6340-1,x + sta COLLISION_TILES-1,x @skip1: lda $6000-1+$d0,x jsr CheckForSolidMTiles bcc @skip2 - sta $6340-1+$d0,x + sta COLLISION_TILES-1+$d0,x @skip2: lda $6000-1+$d0*2,x jsr CheckForSolidMTiles bcc @skip3 - sta $6340-1+$d0*2,x + sta COLLISION_TILES-1+$d0*2,x @skip3: lda $6000-1+$d0*3,x jsr CheckForSolidMTiles bcc @skip4 - sta $6340-1+$d0*3,x + sta COLLISION_TILES-1+$d0*3,x @skip4: dex bne @loop diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 353bfb04b..baafa8116 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -436,7 +436,9 @@ SoftDisableNmi = $6382 ; Flag to store if the dialog has ended DialogCompleteTmp = $0523 -StatTrackingBase = $6383 +COLLISION_TILES = $6390 + +StatTrackingBase = COLLISION_TILES + $d0 * 4 SET_RES_BASE StatTrackingBase ;; The follow stats are NOT checkpointed because they persist between reloads From 64e7932b825c884a43f02ca5959a34ad821c0f41 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 22 Sep 2025 13:11:55 -0400 Subject: [PATCH 004/128] wip --- CommandLine/Sample.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CommandLine/Sample.json b/CommandLine/Sample.json index e38e0773e..2e5bb87e8 100644 --- a/CommandLine/Sample.json +++ b/CommandLine/Sample.json @@ -171,5 +171,6 @@ "UseCommunityText": true, "UseCustomRooms": false, "VanillaShuffleUsesActualTerrain": true, -"WestBiome": "RANDOM_NO_VANILLA_OR_SHUFFLE" +"WestBiome": "RANDOM_NO_VANILLA_OR_SHUFFLE", +"MarioMode": true } \ No newline at end of file From 1df5458c671f75f71564bec3202e86114f310d47 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 27 Jan 2026 02:12:28 -0500 Subject: [PATCH 005/128] Back in action finally --- CommandLine/Program.cs | 4 +- CommandLine/Properties/launchSettings.json | 2 +- CommandLine/Sample.json | 4 +- CrossPlatformUI/Views/Tabs/SpellsView.axaml | 2 +- Desktop.Common/DesktopJsEngine.cs | 2 +- Directory.Build.props | 6 + RandomizerCore/Asm/MMC5.s | 3 + RandomizerCore/Asm/z2mario/integration.s | 113 +- RandomizerCore/Asm/z2mario/map.s | 7 +- RandomizerCore/Asm/z2mario/mario.s | 2885 +++++++++++++++++ RandomizerCore/Asm/z2mario/metasprite.s | 729 +++++ .../Asm/z2mario/metasprite_engine.s | 722 ----- RandomizerCore/Asm/z2mario/sfx.s | 4 + RandomizerCore/Asm/z2mario/sprites_mario.chr | Bin 0 -> 4096 bytes RandomizerCore/Asm/z2r.inc | 30 +- RandomizerCore/Hyrule.cs | 30 +- RandomizerCore/RandomizerCore.csproj | 14 + 17 files changed, 3806 insertions(+), 751 deletions(-) create mode 100644 RandomizerCore/Asm/z2mario/mario.s delete mode 100644 RandomizerCore/Asm/z2mario/metasprite_engine.s create mode 100644 RandomizerCore/Asm/z2mario/sprites_mario.chr diff --git a/CommandLine/Program.cs b/CommandLine/Program.cs index 63c085c7f..b6c645011 100644 --- a/CommandLine/Program.cs +++ b/CommandLine/Program.cs @@ -137,9 +137,8 @@ public async Task Randomize() var randomizer = new Hyrule(createAsm,palaceRooms); var rom = await randomizer.Randomize(vanillaRomData!, configuration, UpdateProgress, cts.Token); - if (rom.romdata != null) + if (rom.success) { - char os_sep = Path.DirectorySeparatorChar; var filename = Rom!; var outpath = OutputPath; @@ -163,6 +162,7 @@ public async Task Randomize() else { logger.Error("An exception occurred generating the rom"); + logger.Error(rom.messages); } } diff --git a/CommandLine/Properties/launchSettings.json b/CommandLine/Properties/launchSettings.json index 94091d448..f6598c862 100644 --- a/CommandLine/Properties/launchSettings.json +++ b/CommandLine/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "CommandLine": { "commandName": "Project", - "commandLineArgs": "-c ../../../Sample.json -r ../../../../Zelda2.nes -s 12345678 -o ../../../.." + "commandLineArgs": "-f AAAA2dJAJBAAAAAAABALyPyk+hAAAAAAAAAAAAABAC -r ../../../../Zelda2.nes -s 12345678 -o ../../../.." } } } \ No newline at end of file diff --git a/CommandLine/Sample.json b/CommandLine/Sample.json index 2e5bb87e8..83c3d7bbe 100644 --- a/CommandLine/Sample.json +++ b/CommandLine/Sample.json @@ -1,14 +1,14 @@ { "AllowConnectionCavesToBeBlocked": false, "AllowUnsafePathEncounters": false, -"AttackEffectiveness": "HIGH", +"AttackEffectiveness": "DEFAULT", "AttackLevelCap": 8, "BeamSprite": "DEFAULT", "BeepFrequency": "Off", "BeepThreshold": "Normal", "BlockingRoomsInAnyPalace": false, "BossRoomsExitType": "OVERWORLD", -"ChangePalacePallettes": true, +"ChangePalacePallettes": false, "Climate": { "Name": "Classic", diff --git a/CrossPlatformUI/Views/Tabs/SpellsView.axaml b/CrossPlatformUI/Views/Tabs/SpellsView.axaml index d39fb3574..7f19ccb2b 100644 --- a/CrossPlatformUI/Views/Tabs/SpellsView.axaml +++ b/CrossPlatformUI/Views/Tabs/SpellsView.axaml @@ -76,7 +76,7 @@ diff --git a/Desktop.Common/DesktopJsEngine.cs b/Desktop.Common/DesktopJsEngine.cs index 3e0c064f1..1895eb1ae 100644 --- a/Desktop.Common/DesktopJsEngine.cs +++ b/Desktop.Common/DesktopJsEngine.cs @@ -4,7 +4,7 @@ namespace Desktop.Common; [UnsupportedOSPlatform("browser")] -public class DesktopJsEngine(Js65Options? options = null, bool debugJavascript = false) +public class DesktopJsEngine(Js65Options? options = null, bool debugJavascript = true) #pragma warning disable CA1416 : ClearScriptEngine(options, true, debugJavascript); #pragma warning restore CA1416 diff --git a/Directory.Build.props b/Directory.Build.props index 576f389f9..bd965de9d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,6 +2,12 @@ net10.0 enable + 1.0.7 + 11.3.12 + 0.10.4 + 3.13.4 + 2.4.1 + 2.88.9 true false true diff --git a/RandomizerCore/Asm/MMC5.s b/RandomizerCore/Asm/MMC5.s index 58046db7d..ee634661f 100644 --- a/RandomizerCore/Asm/MMC5.s +++ b/RandomizerCore/Asm/MMC5.s @@ -531,6 +531,9 @@ SwapCHR: adc #1 sta SpChrBank4Reg sta BgChrBank0Reg +.ifdef ENABLE_Z2_MARIO + sta CurrentCHRBank ; Store a copy of the latest BG bank for use in z2mario +.endif adc #1 sta SpChrBank5Reg sta BgChrBank1Reg diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 2dd833d9a..04f70a730 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1,4 +1,12 @@ +.include "z2r.inc" + +.import GameRoutines, ProcFireball_Bubble, PlayerGfxHandler, DrawMetasprite +.import Square1SfxHandler, Square2SfxHandler, NoiseSfxHandler +.import SwapToSavedPRG, SwapToPRG0 + +.export SlightlyModifiedCollisionRoutine + ; Remove unused code after we gutted link's movement FREE "PRG0" [$92BF, $962D) @@ -21,17 +29,31 @@ BankSwitchMarioCHR: beq + ; .repeat 8, I ; lda CurrentCHRBank + I -; sta MMC5_CHR_BANK_BASE + I +; sta SpChrBank0Reg + I ; .endrepeat ; brk ; nop lda PlayerChrBank - sta MMC5_CHR_BANK_BASE + 0 + sta SpChrBank0Reg + 0 lda CurrentCHRBank ; we need to write a BG bank register due to MMC5 jank - sta MMC5_CHR_BANK_BASE + 8 + sta BgChrBank0Reg + rts +; patch the link draw on lives screen to set the y hi position properly +.org $c3fb + sta Player_X_Position ; Use the regular X position instead of the "screen" version +.org $C40D + jmp PatchLinkLivesScreenDraw +.reloc +PatchLinkLivesScreenDraw: + lda #1 + sta Player_Y_HighPos + ; also force the bank to be correct + jsr PlayerGfxHandler + jsr BankSwitchMarioCHR + jmp $EC02 + ; patch the link draw routine to skip drawing him with the vanilla code .org $EC02 ; ldx $11 lda $29,x jsr PatchLinkDrawRoutine @@ -92,7 +114,7 @@ PatchLinkDrawRoutine: jsr SetupMarioControl .reloc SetupMarioControl: - sta $69DE +; sta $69DE ; This is the color index 3 for the firey palette lda #8 sta GameEngineSubroutine lda #1 ; Ground area type @@ -326,15 +348,20 @@ SlightlyModifiedCollisionRoutine: ; nop ; Update palette locations -.ifdef DEMO_CODE - UPDATE_BYTE $37 @ $285a $2a0a $40af $40bf $40cf $40df $80af $80bf $80cf $80df $c0af $c0bf $c0cf $c0df $c0ef $100af $100bf $100cf $100df $140af $140bf $140cf $140df $17c19 $1c464 $1c47c - UPDATE_BYTE $27 @ $285b $2a0b $2a10 $40b0 $40c0 $40d0 $40e0 $80b0 $80c0 $80d0 $80e0 $c0b0 $c0c0 $c0d0 $c0e0 $c0f0 $100b0 $100c0 $100d0 $100e0 $140b0 $140c0 $140d0 $140e0 $17c1a $1c465 $1c47d - UPDATE_BYTE $16 @ $285C $40b1 $40c1 $40d1 $80e1 $80b1 $80c1 $80d1 $80e1 $c0b1 $c0c1 $c0d1 $c0e1 $100b1 $100c1 $100d1 $100e1 $140b1 $140c1 $140d1 $140e1 $17c1b $1c466 $1c47e -.else - UPDATE_BYTE $16 @ $285a $2a0a $40af $40bf $40cf $40df $80af $80bf $80cf $80df $c0af $c0bf $c0cf $c0df $c0ef $100af $100bf $100cf $100df $140af $140bf $140cf $140df $17c19 $1c464 $1c47c - UPDATE_BYTE $27 @ $285b $2a0b $2a10 $40b0 $40c0 $40d0 $40e0 $80b0 $80c0 $80d0 $80e0 $c0b0 $c0c0 $c0d0 $c0e0 $c0f0 $100b0 $100c0 $100d0 $100e0 $140b0 $140c0 $140d0 $140e0 $17c1a $1c465 $1c47d +;.ifdef DEMO_CODE +; UPDATE_BYTE $37 @ $285a $2a0a $40af $40bf $40cf $40df $80af $80bf $80cf $80df $c0af $c0bf $c0cf $c0df $c0ef $100af $100bf $100cf $100df $140af $140bf $140cf $140df $17c19 $1c464 $1c47c +; UPDATE_BYTE $27 @ $285b $2a0b $2a10 $40b0 $40c0 $40d0 $40e0 $80b0 $80c0 $80d0 $80e0 $c0b0 $c0c0 $c0d0 $c0e0 $c0f0 $100b0 $100c0 $100d0 $100e0 $140b0 $140c0 $140d0 $140e0 $17c1a $1c465 $1c47d +; UPDATE_BYTE $16 @ $285C $40b1 $40c1 $40d1 $80e1 $80b1 $80c1 $80d1 $80e1 $c0b1 $c0c1 $c0d1 $c0e1 $100b1 $100c1 $100d1 $100e1 $140b1 $140c1 $140d1 $140e1 $17c1b $1c466 $1c47e +;.else + UPDATE_BYTE $16 @ $285a $40af $40bf $40cf $40df $80af $80bf $80cf $80df $c0af $c0bf $c0cf $c0df $c0ef $100af $100bf $100cf $100df $140af $140bf $140cf $140df $17c19 $1c464 $1c47c + UPDATE_BYTE $27 @ $285b $40b0 $40c0 $40d0 $40e0 $80b0 $80c0 $80d0 $80e0 $c0b0 $c0c0 $c0d0 $c0e0 $c0f0 $100b0 $100c0 $100d0 $100e0 $140b0 $140c0 $140d0 $140e0 $17c1a $1c465 $1c47d UPDATE_BYTE $18 @ $285C $40b1 $40c1 $40d1 $80e1 $80b1 $80c1 $80d1 $80e1 $c0b1 $c0c1 $c0d1 $c0e1 $100b1 $100c1 $100d1 $100e1 $140b1 $140c1 $140d1 $140e1 $17c1b $1c466 $1c47e -.endif +;.endif +; Fire spell color +UPDATE_BYTE $37 @ $2a0a +UPDATE_BYTE $27 @ $2a10 +UPDATE_BYTE $16 @ $2a16 + ; regular palette color location UPDATE_BYTE $18 $10ea @@ -524,3 +551,65 @@ CheckForDownstab: .org $9A42 jmp * + 4 ; skip a hardcoded metasprite update when reading signs +; Patch the main menu to use the overworld mario sprite +.segment "PRG5" +; Remove the code that draws the link sprite on the menu +FREE "PRG5" [$B596, $B5B5) +.org $B392 + jmp *+3 ; skip drawing menu link +.org $B4EE + jmp *+3 ; skip drawing menu link +.org $B6FD + jmp *+3 ; skip drawing menu link + +; Update the fairy to draw two sprites of mario instead +FREE "PRG5" [$B574, $B596) +.org $B38B + jsr DrawMapMario +.org $B4E7 + jsr DrawMapMario +.org $B6F6 + jsr DrawMapMario + +.reloc +; maps from the timer to the map mario animation frame +MarioMapMappingMapper: + .byte $20, $20, $20, $20 + .byte $24, $24, $24, $24 + .byte $28, $28, $28, $28 + .byte $24, $24, $24, $24 + +.reloc +DrawMapMario: + ; $1c = current sprite tile + ; $1d = timer + ; Y = current Y position + + ; Force bank in the mario map sprite + lda #$46 + sta SpChrBank0Reg + 0 + lda CurrentCHRBank ; we need to write a BG bank register due to MMC5 jank + sta BgChrBank0Reg + inc $1d ; increate the timer + lda $1d + lsr + and #$0f + tax + ; Map the timer to the sprite that we want to use + lda MarioMapMappingMapper,x + sta Sprite_Tilenumber+12 + clc + adc #2 + sta Sprite_Tilenumber+16 + sty Sprite_Y_Position+12 + sty Sprite_Y_Position+16 + lda #$40 + sta Sprite_Attributes+12 + sta Sprite_Attributes+16 + lda $4d + sta Sprite_X_Position+16 +; clc ; cleared above + adc #8 + sta Sprite_X_Position+12 + + rts diff --git a/RandomizerCore/Asm/z2mario/map.s b/RandomizerCore/Asm/z2mario/map.s index 40bb60d63..2c25c4426 100644 --- a/RandomizerCore/Asm/z2mario/map.s +++ b/RandomizerCore/Asm/z2mario/map.s @@ -1,9 +1,12 @@ + +.include "z2r.inc" + .segment "PRG0" ; UPDATE_BYTE $00 @ $0cef ;update mario palette in overworld -.org $8cdf - .byte $00 +;.org $8cdf +; .byte $00 ; update raft tiles .org $81C0 lda #$b1 diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s new file mode 100644 index 000000000..47d714a2e --- /dev/null +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -0,0 +1,2885 @@ + +.include "z2r.inc" + +.import SlightlyModifiedCollisionRoutine +.import PlayerBankTable +.import METASPRITE_SMALL_MARIO_DEATH, FIRE_MARIO_OFFSET, METASPRITE_FIRE_MARIO_SWIMMING_STILL_1, METASPRITE_SMALL_FIRE_SWIMMING_STILL_1 +.import METASPRITE_BIG_MARIO_JUMPING, METASPRITE_BIG_MARIO_SWIMMING_1_KICK, METASPRITE_BIG_MARIO_STANDING, METASPRITE_BIG_MARIO_SKIDDING +.import METASPRITE_BIG_MARIO_WALKING_1, METASPRITE_BIG_MARIO_CLIMBING_1, METASPRITE_BIG_MARIO_CROUCHING, METASPRITE_FIRE_MARIO_STANDING +.import METASPRITE_SMALL_MARIO_JUMPING, METASPRITE_SMALL_MARIO_SWIMMING_1_KICK, METASPRITE_SMALL_MARIO_STANDING, METASPRITE_SMALL_MARIO_SKIDDING +.import METASPRITE_SMALL_MARIO_WALKING_1, METASPRITE_SMALL_MARIO_CLIMBING_1, METASPRITE_SMALL_MARIO_GROW_STANDING, METASPRITE_FIREBALL_FRAME_1 +.import METASPRITE_FIREBALL_FRAME_2, METASPRITE_EXPLOSION_FRAME_1, METASPRITE_EXPLOSION_FRAME_2, METASPRITE_EXPLOSION_FRAME_3 +.import SWIMMING_ANIMATION_FRAME_COUNT + +.export GameRoutines, ProcFireball_Bubble, PlayerGfxHandler + +.segment "PRG0" +; .reloc + +;------------------------------------------------------------------------------------- + +.proc GameRoutines +;.ifdef DEMO_CODE + lda #1 + sta $0785 ;candle + sta $078c ;key + sta $078b ;hammer + sta $0788 ;boots + sta $0787 ;raft + sta $077f ; fire spell + lda #8 + sta $0783 ; magic jar count + sta $0778 ; magic level + + lda #$ff + sta $0773 ;infinite magic + sta $0774 ;infinite health +;.endif + + lda GameEngineSubroutine ;run routine based on number (a few of these routines are + jsr JumpEngine ;merely placeholders as conditions for other routines) + + .word Entrance_GameTimerSetup + .word Vine_AutoClimb + .word SideExitPipeEntry + .word VerticalPipeEntry + .word FlagpoleSlide + .word PlayerEndLevel + .word PlayerLoseLife + .word PlayerEntrance + .word PlayerCtrlRoutine + .word PlayerChangeSize + .word PlayerInjuryBlink + .word PlayerDeath + .word PlayerFireFlower +.endproc + + +;------------------------------------------------------------------------------------- +; ; .reloc +PlayerEntrance: + lda AltEntranceControl ;check for mode of alternate entry + cmp #$02 + beq EntrMode2 ;if found, branch to enter from pipe or with vine + lda #$00 + ldy Player_Y_Position ;if vertical position above a certain + cpy #$30 ;point, nullify controller bits and continue + bcc AutoControlPlayer ;with player movement code, do not return + lda PlayerEntranceCtrl ;check player entry bits from header + cmp #$06 + beq ChkBehPipe ;if set to 6 or 7, execute pipe intro code + cmp #$07 ;otherwise branch to normal entry + bne PlayerRdy +ChkBehPipe: + ; lda InPipeTransition + lda Player_SprAttrib ;check for sprite attributes + bne IntroEntr ;branch if found + lda #$01 + jmp AutoControlPlayer ;force player to walk to the right +IntroEntr: + jsr EnterSidePipe ;execute sub to move player to the right + dec ChangeAreaTimer ;decrement timer for change of area + bne ExitEntr ;branch to exit if not yet expired + ; inc DisableIntermediate ;set flag to skip world and lives display + ; jmp NextArea ;jump to increment to next area and set modes +EntrMode2: + lda JoypadOverride ;if controller override bits set here, + bne VineEntr ;branch to enter with vine + ; lda #3 + ; jsr SetupPipeTransitionOverlay + lda #$ff ;otherwise, set value here then execute sub + jsr MovePlayerYAxis ;to move player upwards + lda Player_Y_Position ;check to see if player is at a specific coordinate + cmp #$91 ;if player risen to a certain point (this requires pipes + bcs @ContinuePipeEntry ;to be at specific height to look/function right) branch + ; .import FRAME_LAG_COUNT + ; lda #FRAME_LAG_COUNT + ; lda #0 + ; sta PipeExitTimer + ; jsr SetupPipeTransitionOverlay + jmp PlayerRdy +@ContinuePipeEntry: + rts ;to the last part, otherwise leave +VineEntr: + lda Vine_Height + cmp #$60 ;check vine height + bne ExitEntr ;if vine not yet reached maximum height, branch to leave + lda Player_Y_Position ;get player's vertical coordinate + cmp #$99 ;check player's vertical coordinate against preset value + ldy #$00 ;load default values to be written to + lda #$01 ;this value moves player to the right off the vine + bcc OffVine ;if vertical coordinate < preset value, use defaults + lda #$03 + sta Player_State ;otherwise set player state to climbing + iny ;increment value in Y + ; lda #$08 ;set block in block buffer to cover hole, then + ; sta Block_Buffer_1+$b4 ;use same value to force player to climb +OffVine: + sty DisableCollisionDet ;set collision detection disable flag + jsr AutoControlPlayer ;use contents of A to move player up or right, execute sub + lda Player_X_Position + cmp #$48 ;check player's horizontal position + bcc ExitEntr ;if not far enough to the right, branch to leave +PlayerRdy: + lda #$08 ;set routine to be executed by game engine next frame + sta GameEngineSubroutine + lda #$01 ;set to face player to the right + sta PlayerFacingDir + lsr ;init A + sta AltEntranceControl ;init mode of entry + sta DisableCollisionDet ;init collision detection disable flag + sta JoypadOverride ;nullify controller override bits +ExitEntr: + rts ;leave! + + +;------------------------------------------------------------------------------------- +;$07 - used to hold upper limit of high byte when player falls down hole +AutoControlPlayer: + sta SavedJoypadBits ;override controller bits with contents of A if executing here + +PlayerCtrlRoutine: + lda GameEngineSubroutine ;check task here + ; prevent moving on death + cmp #$0b + beq SizeChk + lda AreaType ;are we in a water type area? + bne SaveJoyp ;if not, branch + ldy Player_Y_HighPos + dey ;if not in vertical area between + bne DisJoyp ;status bar and bottom, branch + lda Player_Y_Position + cmp #$d0 ;if nearing the bottom of the screen or + bcc SaveJoyp ;not in the vertical area between status bar or bottom, +DisJoyp: + lda #$00 ;disable controller bits + sta SavedJoypadBits +SaveJoyp: + lda SavedJoypadBits ;otherwise store A and B buttons in $0a + and #%11000000 + sta A_B_Buttons + lda SavedJoypadBits ;store left and right buttons in $0c + and #%00000011 + sta Left_Right_Buttons + lda SavedJoypadBits ;store up and down buttons in $0b + and #%00001100 + sta Up_Down_Buttons + + and #%00000100 ;check for pressing down + beq SizeChk ;if not, branch + lda Player_State ;check player's state + bne SizeChk ;if not on the ground, branch + ldy Left_Right_Buttons ;check left and right + beq SizeChk ;if neither pressed, branch + lda #$00 + sta Left_Right_Buttons ;if pressing down while on the ground, + sta Up_Down_Buttons ;nullify directional bits +SizeChk: + jsr PlayerMovementSubs ;run movement subroutines + ldy #$01 ;is player small? + lda PlayerSize + bne ChkMoveDir + ldy #$00 ;check for if crouching + lda CrouchingFlag + beq ChkMoveDir ;if not, branch ahead + ldy #$02 ;if big and crouching, load y with 2 +ChkMoveDir: + ; sty Player_BoundBoxCtrl ;set contents of Y as player's bounding box size control + lda #$01 ;set moving direction to right by default + ldy Player_X_Speed ;check player's horizontal speed + beq PlayerSubs ;if not moving at all horizontally, skip this part + bpl SetMoveDir ;if moving to the right, use default moving direction + asl ;otherwise change to move to the left + SetMoveDir: + sta Player_MovingDir ;set moving direction +PlayerSubs: + + ; jsr $9610 ; update mario scroll position + + ; lda $a7 ; flip collision bits to match mario style + ; eor #$ff + ; sta $a7 + + jsr ScrollHandler ;move the screen if necessary + ; jsr GetPlayerOffscreenBits ;get player's offscreen bits + ; jsr RelativePlayerPosition ;get coordinates relative to the screen + ; ldx #$00 ;set offset for player object + ; jsr BoundingBoxCore ;get player's bounding box coordinates + jsr PlayerBGCollision ;do collision detection and process + + ; This is the offset for the table that holds the Y add val for link + ldy #$1d + ; Jump to the middle of the original link collision routine + ; jsr $e070 ; link original bg collision routine + jsr SlightlyModifiedCollisionRoutine + + rts + +; lda Player_Y_Position +; cmp #$40 ;check to see if player is higher than 64th pixel +; bcc PlayerHole ;if so, branch ahead +; lda GameEngineSubroutine +; cmp #$05 ;if running end-of-level routine, branch ahead +; beq PlayerHole +; cmp #$07 ;if running player entrance routine, branch ahead +; beq PlayerHole +; cmp #$04 ;if running routines $00-$03, branch ahead +; bcc PlayerHole +; lda Player_SprAttrib +; and #%11011111 ;otherwise nullify player's +; sta Player_SprAttrib ;background priority flag +; PlayerHole: +; lda GameEngineSubroutine +; cmp #$0a ; Ignore falling into hole check if taking damage +; beq ExitCtrl +; lda Player_Y_HighPos ;check player's vertical high byte +; cmp #$02 ;for below the screen +; bmi ExitCtrl ;branch to leave if not that far down +; ldx #$01 +; stx ScrollLock ;set scroll lock +; ldy #$04 +; sty R7 ;set value here +; ldx #$00 ;use X as flag, and clear for cloud level +; ; ldy GameTimerExpiredFlag ;check game timer expiration flag +; ; bne HoleDie ;if set, branch +; ; ldy CloudTypeOverride ;check for cloud type override +; ; bne ChkHoleX ;skip to last part if found +; ; HoleDie: +; inx ;set flag in X for player death +; ldy GameEngineSubroutine +; cpy #$0b ;check for some other routine running +; beq ChkHoleX ;if so, branch ahead +; ldy DeathMusicLoaded ;check value here +; bne HoleBottom ;if already set, branch to next part +; iny +; sty EventMusicQueue ;otherwise play death music +; sty DeathMusicLoaded ;and set value here +; HoleBottom: +; ldy #$06 +; sty R7 +; ChkHoleX: +; cmp R7 ;compare vertical high byte with value set here +; bmi ExitCtrl ;if less, branch to leave +; dex ;otherwise decrement flag in X +; bmi CloudExit ;if flag was clear, branch to set modes and other values +; CheckMusicFinished: +; ldy DeathMusicLoaded ;check to see if music is still playing +; beq DeathMusicEnded ;branch to leave if so +; lda EventMusicBuffer +; ora EventMusicQueue +; bne ExitCtrl +; DeathMusicEnded: +; lda #$06 ;otherwise set to run lose life routine +; sta GameEngineSubroutine ;on next frame +; ExitCtrl: +; jmp PlayerGfxHandler +; rts ;leave + +; CloudExit: +; lda #$00 +; sta JoypadOverride ;clear controller override bits if any are set +; jsr SetEntr ;do sub to set secondary mode +; inc AltEntranceControl ;set mode of entry to 3 + rts + +;------------------------------------------------------------------------------------- +; .reloc +Vine_AutoClimb: + lda Player_Y_HighPos ;check to see whether player reached position + bne AutoClimb ;above the status bar yet and if so, set modes + lda Player_Y_Position + cmp #$e4 + bcc SetEntr +AutoClimb: + lda #%00001000 ;set controller bits override to up + sta JoypadOverride + ldy #$03 ;set player state to climbing + sty Player_State + jmp AutoControlPlayer +SetEntr: + lda #$02 ;set starting position to override + sta AltEntranceControl + jmp ChgAreaMode ;set modes + +;------------------------------------------------------------------------------------- +; .reloc + +VerticalPipeEntry: + lda #$01 ;set 1 as movement amount + jsr MovePlayerYAxis ;do sub to move player downwards + ; jsr ScrollHandler ;do sub to scroll screen with saved force if necessary + ldy #$00 ;load default mode of entry + lda WarpZoneControl ;check warp zone control variable/flag + bne ChgAreaPipe ;if set, branch to use mode 0 + iny + lda AreaType ;check for castle level type + cmp #$03 + bne ChgAreaPipe ;if not castle type level, use mode 1 + iny + jmp ChgAreaPipe ;otherwise use mode 2 + +MovePlayerYAxis: + clc + adc Player_Y_Position ;add contents of A to player position + sta Player_Y_Position + rts + +;------------------------------------------------------------------------------------- +; .reloc + +SideExitPipeEntry: + jsr EnterSidePipe ;execute sub to move player to the right + ldy #$02 +ChgAreaPipe: dec ChangeAreaTimer ;decrement timer for change of area + bne ExitCAPipe + sty AltEntranceControl ;when timer expires set mode of alternate entry +ChgAreaMode: + ; inc DisableScreenFlag ;set flag to disable screen output + ; lda #$00 + ; sta OperMode_Task ;set secondary mode of operation + ; sta Sprite0HitDetectFlag ;disable sprite 0 check + ; jsr SetupPipeTransitionOverlay + ; lda #0 ; make sure we have the value 0 set since its used right after +ExitCAPipe: rts ;leave + +EnterSidePipe: + lda #$08 ;set player's horizontal speed + sta Player_X_Speed +; lda InPipeTransition +; bne :+ +; lda #2 +; jsr SetupPipeTransitionOverlay +; : + ldy #$01 ;set controller right button by default + lda Player_X_Position ;mask out higher nybble of player's + and #%00001111 ;horizontal position + bne RightPipe + sta Player_X_Speed ;if lower nybble = 0, set as horizontal speed + tay ;and nullify controller bit override here +RightPipe: + tya ;use contents of Y to + jmp AutoControlPlayer ;execute player control routine with ctrl bits nulled + +;------------------------------------------------------------------------------------- +; .reloc +;page numbers are in order from -1 to -4 +; HalfwayPageNybbles: +; .byte $56, $40 +; .byte $65, $70 +; .byte $66, $40 +; .byte $66, $40 +; .byte $66, $40 +; .byte $66, $60 +; .byte $65, $70 +; .byte $00, $00 + +.proc PlayerLoseLife + ; inc DisableScreenFlag ;disable screen and sprite 0 check + ; lda #$00 + ; sta Sprite0HitDetectFlag +; lda #Silence ;silence music +; sta EventMusicQueue +; dec NumberofLives ;take one life from player +; bpl StillInGame ;if player still has lives, branch +; ; lda #$00 +; ; sta OperMode_Task ;initialize mode task, +; ; lda #MODE_GAMEOVER ;switch to game over mode +; ; sta OperMode ;and leave +; rts +; StillInGame: +; lda WorldNumber ;multiply world number by 2 and use +; asl ;as offset +; tax +; lda LevelNumber ;if in area -3 or -4, increment +; and #$02 ;offset by one byte, otherwise +; beq GetHalfway ;leave offset alone +; inx +; GetHalfway: +; ldy HalfwayPageNybbles,x ;get halfway page number with offset +; lda LevelNumber ;check area number's LSB +; lsr +; tya ;if in area -2 or -4, use lower nybble +; bcs MaskHPNyb +; lsr ;move higher nybble to lower if area +; lsr ;number is -1 or -3 +; lsr +; lsr +; MaskHPNyb: +; and #%00001111 ;mask out all but lower nybble +; cmp ScreenLeft_PageLoc +; beq SetHalfway ;left side of screen must be at the halfway page, +; bcc SetHalfway ;otherwise player must start at the +; lda #$00 ;beginning of the level +; SetHalfway: +; sta HalfwayPage ;store as halfway page for player +; jsr TransposePlayers ;switch players around if 2-player game + + rts +; jmp RunGameOver::ContinueGame ;continue the game +.endproc + +; .reloc + +PlayerStarting_X_Pos: + .byte $28, $18 + .byte $38, $28 + +AltYPosOffset: + .byte $08, $00 + +PlayerStarting_Y_Pos: + .byte $00, $20, $b0, $50, $00, $00, $b0, $b0 + .byte $f0 + +PlayerBGPriorityData: + .byte $00, $20, $00, $00, $00, $00, $00, $00 + +GameTimerData: + .byte $20 ;dummy byte, used as part of bg priority data + .byte $04, $03, $02 + +Entrance_GameTimerSetup: + lda ScreenLeft_PageLoc ;set current page for area objects + sta Player_PageLoc ;as page location for player + lda #$28 ;store value here + sta VerticalForceDown ;for fractional movement downwards if necessary + lda #$01 ;set high byte of player position and + sta PlayerFacingDir ;set facing direction so that player faces right + sta Player_Y_HighPos + lda #$00 ;set player state to on the ground by default + sta Player_State + sta Player_CollisionBits ;initialize player's collision bits + ldy #$00 ;initialize halfway page + sty HalfwayPage + lda AreaType ;check area type + bne ChkStPos ;if water type, set swimming flag, otherwise do not set + iny +ChkStPos: sty SwimmingFlag + ldx PlayerEntranceCtrl ;get starting position loaded from header + ldy AltEntranceControl ;check alternate mode of entry flag for 0 or 1 + beq SetStPos + cpy #$01 + beq SetStPos + ldx AltYPosOffset-2,y ;if not 0 or 1, override $0710 with new offset in X +SetStPos: lda PlayerStarting_X_Pos,y ;load appropriate horizontal position + sta Player_X_Position ;and vertical positions for the player, using + lda PlayerStarting_Y_Pos,x ;AltEntranceControl as offset for horizontal and either $0710 + sta Player_Y_Position ;or value that overwrote $0710 as offset for vertical + lda PlayerBGPriorityData,x + sta Player_SprAttrib ;set player sprite attributes using offset in X + jsr GetPlayerColors ;get appropriate player palette + ; ldy GameTimerSetting ;get timer control value from header + ; beq ChkOverR ;if set to zero, branch (do not use dummy byte for this) + ; lda FetchNewGameTimerFlag ;do we need to set the game timer? if not, use + ; beq ChkOverR ;old game timer setting + ; lda GameTimerData,y ;if game timer is set and game timer flag is also set, + ; sta GameTimerDisplay ;use value of game timer control for first digit of game timer + ; lda #$01 + ; sta GameTimerDisplay+2 ;set last digit of game timer to 1 + ; lsr + ; sta GameTimerDisplay+1 ;set second digit of game timer + ; sta FetchNewGameTimerFlag ;clear flag for game timer reset + ; sta StarInvincibleTimer ;clear star mario timer +ChkOverR: ldy JoypadOverride ;if controller bits not set, branch to skip this part + beq ChkSwimE + ; lda #$03 ;set player state to climbing + ; sta Player_State + ; ldx #$00 ;set offset for first slot, for block object + ; jsr InitBlock_XY_Pos + ; lda #$f0 ;set vertical coordinate for block object + ; sta Block_Y_Position + ; ldx #$05 ;set offset in X for last enemy object buffer slot + ; ldy #$00 ;set offset in Y for object coordinates used earlier + ; farcall Setup_Vine ;do a sub to grow vine +ChkSwimE: ldy AreaType ;if level not water-type, + bne SetPESub ;skip this subroutine + ; farcall SetupBubble ;otherwise, execute sub to set up air bubbles +SetPESub: lda #$07 ;set to run player entrance subroutine + sta GameEngineSubroutine ;on the next frame of game engine + rts + + +;------------------------------------------------------------------------------------- +;$00 - used to store player's vertical offscreen bits +; .reloc +.proc PlayerGfxHandler + lda InjuryTimer ;if player's injured invincibility timer + beq CntPl ;not set, skip checkpoint and continue code + lda FrameCounter + lsr ;otherwise check frame counter and branch + bcs ClearMarioSprite ;to leave on every other frame (when d0 is set) +CntPl: + lda GameEngineSubroutine ;if executing specific game engine routine, + cmp #$0b ;branch ahead to some other part + beq PlayerKilled + lda PlayerChangeSizeFlag ;if grow/shrink flag set + bne DoChangeSize ;then branch to some other code + + jsr FindPlayerAction ;otherwise jump and return + + lda SwimmingFlag + beq Exit + ; if the player is standing on the ground, don't animate the leg kicking. + lda Player_State + beq Exit + ; if the player is swimming, every 8 frames switch metasprite to use the kick animation + lda FrameCounter + and #%00000100 ;check frame counter for d2 set (8 frames every + bne Exit ;eighth frame), and branch if set to leave + ; a bit hacky here, but we have two types of offsets. If its one of the glitchy frames, bump the metasprite by one, else + ; add our animation extent offset + lda ObjectMetasprite + cmp #METASPRITE_FIRE_MARIO_SWIMMING_STILL_1 + beq GlitchySprite + cmp #METASPRITE_SMALL_FIRE_SWIMMING_STILL_1 + beq GlitchySprite + clc + adc #SWIMMING_ANIMATION_FRAME_COUNT + sta ObjectMetasprite + bne Exit + GlitchySprite: + ; Go to the next frame of the glitch animation + inc ObjectMetasprite +Exit: + ldy ObjectMetasprite + lda PlayerBankTable,y + cmp PlayerChrBank + beq + + sta PlayerChrBank + inc ReloadCHRBank + + + rts ;then leave + +ClearMarioSprite: + lda #0 + sta ObjectMetasprite + rts +.endproc + +FindPlayerAction: + jsr ProcessPlayerAction ;find proper offset to graphics table by player's actions + jmp PlayerGfxProcessing ;draw player, then process for fireball throwing + +DoChangeSize: + jsr HandleChangeSize ;find proper offset to graphics table for grow/shrink + jmp PlayerGfxProcessing ;draw player, then process for fireball throwing + +PlayerKilled: + ; ldy #$0e ;load offset for player killed + ; lda PlayerGfxTblOffsets,y ;get offset to graphics table + lda #METASPRITE_SMALL_MARIO_DEATH + +PlayerGfxProcessing: + sta ObjectMetasprite + + lda FireballThrowingTimer + beq PlayerOffscreenChk ;if fireball throw timer not set, skip to the end + ldy #$00 ;set value to initialize by default + lda PlayerAnimTimer ;get animation frame timer + cmp FireballThrowingTimer ;compare to fireball throw timer + sty FireballThrowingTimer ;initialize fireball throw timer + bcs PlayerOffscreenChk ;if animation frame timer => fireball throw timer skip to end + sta FireballThrowingTimer ;otherwise store animation timer into fireball throw timer + + ; Change the Mario sprite to the fire version + lda ObjectMetasprite + clc + adc #FIRE_MARIO_OFFSET + sta ObjectMetasprite + + lda Player_X_Speed + ora Left_Right_Buttons ;check for horizontal speed or left/right button press + bne SUpdR ;if no speed or button press, branch using set value in Y + ; Use the glitchy version of the sprite + lda PlayerSize + bne SmallFireMario + lda #METASPRITE_FIRE_MARIO_SWIMMING_STILL_1 + bne SetFrame + SmallFireMario: + lda #METASPRITE_SMALL_FIRE_SWIMMING_STILL_1 + SetFrame: + sta ObjectMetasprite +SUpdR: + +PlayerOffscreenChk: + ; lda SprObject_Y_Position,x ;load vertical coordinate low + ; sta SprObject_Rel_YPos,y ;store here + lda SprObject_X_Position ;load horizontal coordinate + sec ;subtract left edge coordinate + sbc ScreenLeft_X_Pos + ; sta SprObject_Rel_XPos ;store result here + sta Player_Pos_ForScroll + rts ;then we are done! + +PlayerGfxTblOffsets: + ; .byte $20, $28, $c8, $18, $00, $40, $50, $58 + .byte METASPRITE_BIG_MARIO_JUMPING + .byte METASPRITE_BIG_MARIO_SWIMMING_1_KICK + .byte METASPRITE_BIG_MARIO_STANDING + .byte METASPRITE_BIG_MARIO_SKIDDING + .byte METASPRITE_BIG_MARIO_WALKING_1 + .byte METASPRITE_BIG_MARIO_CLIMBING_1 + .byte METASPRITE_BIG_MARIO_CROUCHING + .byte METASPRITE_FIRE_MARIO_STANDING + ; .byte $80, $88, $b8, $78, $60, $a0, $b0, $b8 + .byte METASPRITE_SMALL_MARIO_JUMPING + .byte METASPRITE_SMALL_MARIO_SWIMMING_1_KICK + .byte METASPRITE_SMALL_MARIO_STANDING + .byte METASPRITE_SMALL_MARIO_SKIDDING + .byte METASPRITE_SMALL_MARIO_WALKING_1 + .byte METASPRITE_SMALL_MARIO_CLIMBING_1 + .byte METASPRITE_SMALL_MARIO_DEATH +GrowAnimation = * - PlayerGfxTblOffsets + .byte METASPRITE_SMALL_MARIO_GROW_STANDING + +HandleChangeSize: + ldy PlayerAnimCtrl ;get animation frame control + lda FrameCounter + and #%00000011 ;get frame counter and execute this code every + bne GorSLog ;fourth frame, otherwise branch ahead + iny ;increment frame control + cpy #$0a ;check for preset upper extent + bcc CSzNext ;if not there yet, skip ahead to use + ldy #$00 ;otherwise initialize both grow/shrink flag + sty PlayerChangeSizeFlag ;and animation frame control +CSzNext: + sty PlayerAnimCtrl ;store proper frame control +GorSLog: + lda PlayerSize ;get player's size + bne ShrinkPlayer ;if player small, skip ahead to next part + lda ChangeSizeOffsetAdder,y ;get offset adder based on frame control as offset + ldy #GrowAnimation ;load offset for player growing + +GetOffsetFromAnimCtrl: + ; asl ;multiply animation frame control + ; asl ;by eight to get proper amount + ; asl ;to add to our offset + clc + adc PlayerGfxTblOffsets,y ;add to offset to graphics table + rts ;and return with result in A +ChangeSizeOffsetAdder: + .byte $00, $01, $00, $01, $00, $01, $02, $00, $01, $02 + .byte $02, $00, $02, $00, $02, $00, $02, $00, $02, $00 + +ShrinkPlayer: + tya ;add ten bytes to frame control as offset + clc + adc #$0a ;this thing apparently uses two of the swimming frames + tax ;to draw the player shrinking + ldy #$09 ;load offset for small player swimming + lda ChangeSizeOffsetAdder,x ;get what would normally be offset adder + bne ShrPlF ;and branch to use offset if nonzero + ldy #$01 ;otherwise load offset for big player swimming +ShrPlF: + lda PlayerGfxTblOffsets,y ;get offset to graphics table based on offset loaded + rts ;and leave + + +;------------------------------------------------------------------------------------- +; .reloc +PlayerChangeSize: + lda TimerControl ;check master timer control + cmp #$f8 ;for specific moment in time + bne EndChgSize ;branch if before or after that point + jmp InitChangeSize ;otherwise run code to get growing/shrinking going +EndChgSize: + cmp #$c4 ;check again for another specific moment + bne ExitChgSize ;and branch to leave if before or after that point + jmp DonePlayerTask ;otherwise do sub to init timer control and set routine +ExitChgSize: + rts ; TODO check this RTS can be removed ;and then leave + +;------------------------------------------------------------------------------------- +; .reloc +PlayerInjuryBlink: + lda TimerControl ;check master timer control + cmp #$f0 ;for specific moment in time + bcs ExitBlink ;branch if before that point + cmp #$c8 ;check again for another specific point + beq DonePlayerTask ;branch if at that point, and not before or after + jmp PlayerCtrlRoutine ;otherwise run player control routine +ExitBlink: + bne ExitBoth ;do unconditional branch to leave + +InitChangeSize: + ldy PlayerChangeSizeFlag ;if growing/shrinking flag already set + bne ExitBoth ;then branch to leave + sty PlayerAnimCtrl ;otherwise initialize player's animation frame control + inc PlayerChangeSizeFlag ;set growing/shrinking flag + lda PlayerSize + eor #$01 ;invert player's size + sta PlayerSize +ExitBoth: + rts ;leave + +;------------------------------------------------------------------------------------- +;$00 - used in CyclePlayerPalette to store current palette to cycle +; .reloc +PlayerDeath: + lda TimerControl ;check master timer control + cmp #$f0 ;for specific moment in time + bcs ExitTask ;branch to leave if before that point + jmp PlayerCtrlRoutine ;otherwise run player control routine + +DonePlayerTask: + lda #$00 + sta TimerControl ;initialize master timer control to continue timers + lda #$08 + sta GameEngineSubroutine ;set player control routine to run next frame +ExitTask: + rts ;leave + +CyclePlayerPalettePreload: + lda R0 + jmp CyclePlayerPalette + +PlayerFireFlower: + lda TimerControl ;check master timer control + cmp #$c0 ;for specific moment in time + beq ResetPalFireFlower ;branch if at moment, not before or after + lda FrameCounter ;get frame counter + lsr + lsr ;divide by four to change every four frames + +CyclePlayerPalette: + and #$03 ;mask out all but d1-d0 (previously d3-d2) + sta R0 ;store result here to use as palette bits + lda Player_SprAttrib ;get player attributes + and #%11111100 ;save any other bits but palette bits + ora R0 ;add palette bits + sta Player_SprAttrib ;store as new player attributes + rts ;and leave + +ResetPalFireFlower: + jsr DonePlayerTask ;do sub to init timer control and run player control routine + +ResetPalStar: + lda Player_SprAttrib ;get player attributes + and #%11111100 ;mask out palette bits to force palette 0 + sta Player_SprAttrib ;store as new player attributes + rts ;and leave + +;------------------------------------------------------------------------------------- +.reloc +FlagpoleSlide: + ; lda Enemy_ID+5 ;check special use enemy slot + ; cmp #FlagpoleFlagObject ;for flagpole flag object + ; bne NoFPObj ;if not found, branch to something residual + ; lda FlagpoleSoundQueue ;load flagpole sound + ; sta Square1SoundQueue ;into square 1's sfx queue + ; lda #$00 + ; sta FlagpoleSoundQueue ;init flagpole sound queue + ldy Player_Y_Position + cpy #$9e ;check to see if player has slid down + bcs SlidePlayer ;far enough, and if so, branch with no controller bits set + lda #$04 ;otherwise force player to climb down (to slide) +SlidePlayer: + jmp AutoControlPlayer ;jump to player control routine +; NoFPObj: inc GameEngineSubroutine ;increment to next routine (this may +; rts ;be residual code) + +;------------------------------------------------------------------------------------- +.reloc +; Hidden1UpCoinAmts: +; .byte $15, $23, $16, $1b, $17, $18, $23, $63 + +PlayerEndLevel: + lda #$01 ;force player to walk to the right + jsr AutoControlPlayer + lda Player_Y_Position ;check player's vertical position + cmp #$ae + bcc ChkStop ;if player is not yet off the flagpole, skip this part +; .if ::USE_SMB2J_FEATURES +; lda #$00 +; sta ScrollLock ;reactivate scroll +; lda FlagpoleMusicFlag ;check flag to see if music was already queued +; bne ChkStop ;if so, skip this +; lda #EndOfLevelMusic +; sta EventMusicQueue ;load win level music in event music queue +; inc FlagpoleMusicFlag ;set flag to keep music from getting queued more than once +; .else + lda ScrollLock ;if scroll lock not set, branch ahead to next part + beq ChkStop ;because we only need to do this part once + ; lda #EndOfLevelMusic + ; sta EventMusicQueue ;load win level music in event music queue + lda #$00 + sta ScrollLock ;turn off scroll lock to skip this part later +; .endif +ChkStop: + ; lda Player_CollisionBits ;get player collision bits + ; lsr ;check for d0 set + ; bcs RdyNextA ;if d0 set, skip to next part +; lda StarFlagTaskControl ;if star flag task control already set, +; bne InCastle ;go ahead with the rest of the code +; inc StarFlagTaskControl ;otherwise set task control now (this gets ball rolling!) +; InCastle: +; lda #%00100000 ;set player's background priority bit to +; sta Player_SprAttrib ;give illusion of being inside the castle +; RdyNextA: +; lda StarFlagTaskControl +; cmp #$05 ;if star flag task control not yet set +; bne ExitNA ;beyond last valid task number, branch to leave +; PrcNextA: +; inc LevelNumber ;increment level number used for game logic +; lda LevelNumber +; cmp #$03 ;check to see if we have yet reached level -4 +; bne NextArea ;and skip this last part here if not +; ldy WorldNumber ;get world number as offset +; lda CoinTallyFor1Ups ;check third area coin tally for bonus 1-ups +; cmp Hidden1UpCoinAmts,y ;against minimum value, if player has not collected +; bcc NextArea ;at least this number of coins, leave flag clear +; inc Hidden1UpFlag ;otherwise set hidden 1-up box control flag +; NextArea: + + ; jroweboy added: + ; disable the screen since loading a new area pointer will change CHR banks + ; inc DisableScreenFlag + + ; inc AreaNumber ;increment area number used for address loader + ; jsr LoadAreaPointer ;get new level pointer + ; inc FetchNewGameTimerFlag ;set flag to load new game timer + ; jsr ChgAreaMode ;do sub to set secondary mode, disable screen and sprite 0 + ; sta HalfwayPage ;reset halfway page to 0 (beginning) + ; lda #Silence + ; sta EventMusicQueue ;silence music and leave +ExitNA: + rts + +;------------------------------------------------------------------------------------- +.reloc +PlayerMovementSubs: + lda #$00 ;set A to init crouch flag by default + ldy PlayerSize ;is player small? + bne SetCrouch ;if so, branch + lda Player_State ;check state of player + bne ProcMove ;if not on the ground, branch + + ; ZELDA added check for on elevator. If on elevator set standing + lda $0754 + beq NotZ2Elevator + lda #0 + beq SetCrouch +NotZ2Elevator: + lda Up_Down_Buttons ;load controller bits for up and down + and #%00000100 ;single out bit for down button +SetCrouch: + sta CrouchingFlag ;store value in crouch flag + tay + lda CrouchingFlagConvert,y + sta $17 ; Zelda 2 CrouchingFlag +ProcMove: + jsr PlayerPhysicsSub ;run sub related to jumping and swimming + lda PlayerChangeSizeFlag ;if growing/shrinking flag set, + bne NoMoveSub ;branch to leave + lda Player_State + cmp #$03 ;get player state + beq MoveSubs ;if climbing, branch ahead, leave timer unset + ldy #$18 + sty ClimbSideTimer ;otherwise reset timer now +MoveSubs: + jsr JumpEngine +.word OnGroundStateSub +.word JumpSwimSub +.word FallingSub +.word ClimbingSub + +NoMoveSub: rts + +CrouchingFlagConvert: +.byte $01, $00, $00, $00, $00 + +;------------------------------------------------------------------------------------- +;$00 - used by ClimbingSub to store high vertical adder +.reloc +OnGroundStateSub: + jsr GetPlayerAnimSpeed ;do a sub to set animation frame timing + lda Left_Right_Buttons + beq GndMove ;if left/right controller bits not set, skip instruction + sta PlayerFacingDir ;otherwise set new facing direction +GndMove: + jsr ImposeFriction ;do a sub to impose friction on player's walk/run +JmpMove: + jsr MovePlayerHorizontally ;do another sub to move player horizontally + sta Player_X_Scroll ;set returned value as player's movement speed for scroll +; .if ::USE_SMB2J_FEATURES +; farcall BlowPlayerAround +; .endif + rts + +MovePlayerHorizontally: + ; lda JumpspringAnimCtrl ;if jumpspring currently animating, + ; bne ExXMove ;branch to leave + ldx #0 ;otherwise set zero for offset to use player's stuff + jmp MoveObjectHorizontally + +;-------------------------------- + +FallingSub: + lda VerticalForceDown + sta VerticalForce ;dump vertical movement force for falling into main one + jmp LRAir ;movement force, then skip ahead to process left/right movement + +;-------------------------------- + +JumpSwimSub: + ldy Player_Y_Speed ;if player's vertical speed zero + bpl DumpFall ;or moving downwards, branch to falling + lda A_B_Buttons + and #A_Button ;check to see if A button is being pressed + and PreviousA_B_Buttons ;and was pressed in previous frame + bne ProcSwim ;if so, branch elsewhere + lda JumpOrigin_Y_Position ;get vertical position player jumped from + sec + sbc Player_Y_Position ;subtract current from original vertical coordinate + cmp DiffToHaltJump ;compare to value set here to see if player is in mid-jump + bcc ProcSwim ;or just starting to jump, if just starting, skip ahead +DumpFall: +; Force downstab hitbox?? + lda Player_Y_Position + sta $480 + lda VerticalForceDown ;otherwise dump falling into main fractional + sta VerticalForce +ProcSwim: + lda SwimmingFlag ;if swimming flag not set, + beq LRAir ;branch ahead to last part + jsr GetPlayerAnimSpeed ;do a sub to get animation frame timing + lda Player_Y_Position + cmp #$14 ;check vertical position against preset value + bcs LRWater ;if not yet reached a certain position, branch ahead + lda #$18 + sta VerticalForce ;otherwise set fractional +LRWater: + lda Left_Right_Buttons ;check left/right controller bits (check for swimming) + beq LRAir ;if not pressing any, skip + sta PlayerFacingDir ;otherwise set facing direction accordingly +LRAir: + lda Left_Right_Buttons ;check left/right controller bits (check for jumping/falling) + beq JSMove ;if not pressing any, skip + jsr ImposeFriction ;otherwise process horizontal movement +JSMove: +; .if ::USE_SMB2J_FEATURES +; jsr JmpMove +; .else + jsr MovePlayerHorizontally ;do a sub to move player horizontally + sta Player_X_Scroll ;set player's speed here, to be used for scroll later +; .endif + lda GameEngineSubroutine + cmp #$0b ;check for specific routine selected + bne ExitMov1 ;branch if not set to run + lda #$28 + sta VerticalForce ;otherwise set fractional +ExitMov1: + jmp MovePlayerVertically ;jump to move player vertically, then leave + +;-------------------------------- +.reloc +ClimbAdderLow: + .byte $0e, $04, $fc, $f2 +ClimbAdderHigh: + .byte $00, $00, $ff, $ff + +ClimbingSub: + lda Player_YMoveForceFractional + clc ;add movement force to dummy variable + adc Player_Y_MoveForce ;save with carry + sta Player_YMoveForceFractional + ldy #$00 ;set default adder here + lda Player_Y_Speed ;get player's vertical speed + bpl MoveOnVine ;if not moving upwards, branch + dey ;otherwise set adder to $ff +MoveOnVine: sty R0 ;store adder here + adc Player_Y_Position ;add carry to player's vertical position + sta Player_Y_Position ;and store to move player up or down + lda Player_Y_HighPos + adc R0 ;add carry to player's page location + sta Player_Y_HighPos ;and store + lda Left_Right_Buttons ;compare left/right controller bits + and Player_CollisionBits ;to collision flag + beq InitCSTimer ;if not set, skip to end + ldy ClimbSideTimer ;otherwise check timer + bne ExitCSub ;if timer not expired, branch to leave + ldy #$18 + sty ClimbSideTimer ;otherwise set timer now + ldx #$00 ;set default offset here + ldy PlayerFacingDir ;get facing direction + lsr ;move right button controller bit to carry + bcs ClimbFD ;if controller right pressed, branch ahead + inx + inx ;otherwise increment offset by 2 bytes +ClimbFD: dey ;check to see if facing right + beq CSetFDir ;if so, branch, do not increment + inx ;otherwise increment by 1 byte +CSetFDir: lda Player_X_Position + clc ;add or subtract from player's horizontal position + adc ClimbAdderLow,x ;using value here as adder and X as offset + sta Player_X_Position + lda Player_PageLoc ;add or subtract carry or borrow using value here + adc ClimbAdderHigh,x ;from the player's page location + sta Player_PageLoc + lda Left_Right_Buttons ;get left/right controller bits again + eor #%00000011 ;invert them and store them while player + sta PlayerFacingDir ;is on vine to face player in opposite direction +ExitCSub: rts ;then leave +InitCSTimer: sta ClimbSideTimer ;initialize timer here + rts + +;------------------------------------------------------------------------------------- +;$00 - used to store offset to friction data +.reloc +JumpMForceData: + .byte $20, $20, $1e, $28, $28, $0d, $04 + +FallMForceData: + .byte $70, $70, $60, $90, $90, $0a, $09 + +PlayerYSpdData: + .byte $fc, $fc, $fc, $fb, $fb, $fe, $ff + +InitMForceData: + .byte $00, $00, $00, $00, $00, $80, $00 + +MaxLeftXSpdData: + .byte $d8, $e8, $f0 + +MaxRightXSpdData: + .byte $28, $18, $10 + .byte $0c ;used for pipe intros + +FrictionData: + .byte $e4, $98, $d0 + +Climb_Y_SpeedData: + .byte $00, $ff, $01 + +Climb_Y_MForceData: + .byte $00, $20, $ff + +PlayerPhysicsSub: + lda Player_State ;check player state + cmp #$03 + bne CheckForJumping ;if not climbing, branch + ldy #$00 + lda Up_Down_Buttons ;get controller bits for up/down + and Player_CollisionBits ;check against player's collision detection bits + beq ProcClimb ;if not pressing up or down, branch + iny + and #%00001000 ;check for pressing up + bne ProcClimb + iny +ProcClimb: ldx Climb_Y_MForceData,y ;load value here + stx Player_Y_MoveForce ;store as vertical movement force + lda #$08 ;load default animation timing + ldx Climb_Y_SpeedData,y ;load some other value here + stx Player_Y_Speed ;store as vertical speed + bmi SetCAnim ;if climbing down, use default animation timing value + lsr ;otherwise divide timer setting by 2 +SetCAnim: sta PlayerAnimTimerSet ;store animation timer setting and leave + rts + +CheckForJumping: + ; ZELDA added check for on elevator + lda $0754 + bne NoJump + lda JumpspringAnimCtrl ;if jumpspring animating, + bne NoJump ;skip ahead to something else + lda A_B_Buttons ;check for A button press + and #A_Button + beq NoJump ;if not, branch to something else + and PreviousA_B_Buttons ;if button not pressed in previous frame, branch + beq ProcJumping +NoJump: jmp X_Physics ;otherwise, jump to something else + +ProcJumping: + lda Player_State ;check player state + beq InitJS ;if on the ground, branch + lda SwimmingFlag ;if swimming flag not set, jump to do something else + beq NoJump ;to prevent midair jumping, otherwise continue + lda JumpSwimTimer ;if jump/swim timer nonzero, branch + bne InitJS + lda Player_Y_Speed ;check player's vertical speed + bpl InitJS ;if player's vertical speed motionless or down, branch + jmp X_Physics ;if timer at zero and player still rising, do not swim +InitJS: lda #$20 ;set jump/swim timer + sta JumpSwimTimer + ldy #$00 ;initialize vertical force and dummy variable + sty Player_YMoveForceFractional + sty Player_Y_MoveForce + lda Player_Y_HighPos ;get vertical high and low bytes of jump origin + sta JumpOrigin_Y_HighPos ;and store them next to each other here + lda Player_Y_Position + sta JumpOrigin_Y_Position + lda #$01 ;set player state to jumping/swimming + sta Player_State + lda Player_XSpeedAbsolute ;check value related to walking/running speed + cmp #$09 + bcc ChkWtr ;branch if below certain values, increment Y + iny ;for each amount equal or exceeded + cmp #$10 + bcc ChkWtr + iny + cmp #$19 + bcc ChkWtr + iny + cmp #$1c + bcc ChkWtr ;note that for jumping, range is 0-4 for Y + iny +ChkWtr: lda #$01 ;set value here (apparently always set to 1) + sta DiffToHaltJump + lda SwimmingFlag ;if swimming flag disabled, branch + beq GetYPhy + ldy #$05 ;otherwise set Y to 5, range is 5-6 + ; lda Whirlpool_Flag ;if whirlpool flag not set, branch + ; beq GetYPhy + ; iny ;otherwise increment to 6 +GetYPhy: lda JumpMForceData,y ;store appropriate jump/swim + sta VerticalForce ;data here + lda FallMForceData,y + sta VerticalForceDown + lda InitMForceData,y + sta Player_Y_MoveForce + lda PlayerYSpdData,y + sta Player_Y_Speed + lda SwimmingFlag ;if swimming flag disabled, branch + beq PJumpSnd + lda #Sfx_EnemyStomp ;load swim/goomba stomp sound into + sta Square1SoundQueue ;square 1's sfx queue + lda Player_Y_Position + cmp #$14 ;check vertical low byte of player position + bcs X_Physics ;if below a certain point, branch + lda #$00 ;otherwise reset player's vertical speed + sta Player_Y_Speed ;and jump to something else to keep player + jmp X_Physics ;from swimming above water level +PJumpSnd: + lda #Sfx_BigJump ;load big mario's jump sound by default + ldy PlayerSize ;is mario big? + beq SJumpSnd + lda #Sfx_SmallJump ;if not, load small mario's jump sound +SJumpSnd: sta Square1SoundQueue ;store appropriate jump sound in square 1 sfx queue +X_Physics: ldy #$00 + sty R0 ;init value here + lda Player_State ;if mario is on the ground, branch + beq ProcPRun + lda Player_XSpeedAbsolute ;check something that seems to be related + cmp #$19 ;to mario's speed + bcs GetXPhy ;if =>$19 branch here + bcc ChkRFast ;if not branch elsewhere +ProcPRun: iny ;if mario on the ground, increment Y + lda AreaType ;check area type + beq ChkRFast ;if water type, branch + dey ;decrement Y by default for non-water type area + lda Left_Right_Buttons ;get left/right controller bits + cmp Player_MovingDir ;check against moving direction + bne ChkRFast ;if controller bits <> moving direction, skip this part + lda A_B_Buttons ;check for b button pressed + and #B_Button + bne SetRTmr ;if pressed, skip ahead to set timer + lda RunningTimer ;check for running timer set + bne GetXPhy ;if set, branch +ChkRFast: iny ;if running timer not set or level type is water, + inc R0 ;increment Y again and temp variable in memory + lda RunningSpeed + bne FastXSp ;if running speed set here, branch + lda Player_XSpeedAbsolute + cmp #$21 ;otherwise check player's walking/running speed + bcc GetXPhy ;if less than a certain amount, branch ahead +FastXSp: inc R0 ;if running speed set or speed => $21 increment $00 + jmp GetXPhy ;and jump ahead +SetRTmr: lda #$0a ;if b button pressed, set running timer + sta RunningTimer +GetXPhy: lda MaxLeftXSpdData,y ;get maximum speed to the left + sta MaximumLeftSpeed + lda GameEngineSubroutine ;check for specific routine running + cmp #$07 ;(player entrance) + bne GetXPhy2 ;if not running, skip and use old value of Y + ldy #$03 ;otherwise set Y to 3 +GetXPhy2: lda MaxRightXSpdData,y ;get maximum speed to the right + sta MaximumRightSpeed + ldy R0 ;get other value in memory + lda FrictionData,y ;get value using value in memory as offset + sta FrictionAdderLow + lda #$00 + sta FrictionAdderHigh ;init something here + lda PlayerFacingDir + cmp Player_MovingDir ;check facing direction against moving direction + beq ExitPhy ;if the same, branch to leave + asl FrictionAdderLow ;otherwise multiply friction by 2 + rol FrictionAdderHigh ;then leave +ExitPhy: rts + +;------------------------------------------------------------------------------------- +.reloc +PlayerAnimTmrData: + .byte $02, $04, $07 + +GetPlayerAnimSpeed: + ldy #$00 ;initialize offset in Y + lda Player_XSpeedAbsolute ;check player's walking/running speed + cmp #$1c ;against preset amount + bcs SetRunSpd ;if greater than a certain amount, branch ahead + iny ;otherwise increment Y + cmp #$0e ;compare against lower amount + bcs ChkSkid ;if greater than this but not greater than first, skip increment + iny ;otherwise increment Y again +ChkSkid: lda SavedJoypadBits ;get controller bits + and #%01111111 ;mask out A button + beq SetAnimSpd ;if no other buttons pressed, branch ahead of all this + and #$03 ;mask out all others except left and right + cmp Player_MovingDir ;check against moving direction + bne ProcSkid ;if left/right controller bits <> moving direction, branch + lda #$00 ;otherwise set zero value here +SetRunSpd: sta RunningSpeed ;store zero or running speed here + jmp SetAnimSpd +ProcSkid: lda Player_XSpeedAbsolute ;check player's walking/running speed + cmp #$0b ;against one last amount + bcs SetAnimSpd ;if greater than this amount, branch + lda PlayerFacingDir + sta Player_MovingDir ;otherwise use facing direction to set moving direction + lda #$00 + sta Player_X_Speed ;nullify player's horizontal speed + sta Player_X_MoveForce ;and dummy variable for player +SetAnimSpd: lda PlayerAnimTmrData,y ;get animation timer setting using Y as offset + sta PlayerAnimTimerSet + rts + +;------------------------------------------------------------------------------------- +.reloc +ImposeFriction: + and Player_CollisionBits ;perform AND between left/right controller bits and collision flag + ; cmp #$00 ;then compare to zero (this instruction is redundant) + bne JoypFrict ;if any bits set, branch to next part + lda Player_X_Speed + beq SetAbsSpd ;if player has no horizontal speed, branch ahead to last part + bpl RghtFrict ;if player moving to the right, branch to slow + bmi LeftFrict ;otherwise logic dictates player moving left, branch to slow +JoypFrict: + lsr ;put right controller bit into carry + bcc RghtFrict ;if left button pressed, carry = 0, thus branch +LeftFrict: + lda Player_X_MoveForce ;load value set here + clc + adc FrictionAdderLow ;add to it another value set here + sta Player_X_MoveForce ;store here + lda Player_X_Speed + adc FrictionAdderHigh ;add value plus carry to horizontal speed + sta Player_X_Speed ;set as new horizontal speed + cmp MaximumRightSpeed ;compare against maximum value for right movement + bmi XSpdSign ;if horizontal speed greater negatively, branch + lda MaximumRightSpeed ;otherwise set preset value as horizontal speed + sta Player_X_Speed ;thus slowing the player's left movement down + jmp SetAbsSpd ;skip to the end +RghtFrict: + lda Player_X_MoveForce ;load value set here + sec + sbc FrictionAdderLow ;subtract from it another value set here + sta Player_X_MoveForce ;store here + lda Player_X_Speed + sbc FrictionAdderHigh ;subtract value plus borrow from horizontal speed + sta Player_X_Speed ;set as new horizontal speed + cmp MaximumLeftSpeed ;compare against maximum value for left movement + bpl XSpdSign ;if horizontal speed greater positively, branch + lda MaximumLeftSpeed ;otherwise set preset value as horizontal speed + sta Player_X_Speed ;thus slowing the player's right movement down +XSpdSign: + cmp #$00 ;if player not moving or moving to the right, + bpl SetAbsSpd ;branch and leave horizontal speed value unmodified + eor #$ff + clc ;otherwise get two's compliment to get absolute + adc #$01 ;unsigned walking/running speed +SetAbsSpd: + sta Player_XSpeedAbsolute ;store walking/running speed here and leave + rts + +;------------------------------------------------------------------------------------- + +ScrollHandler: + ; lda Player_X_Scroll ;load value saved here + ; clc + ; adc Platform_X_Scroll ;add value used by left/right platforms + ; sta Player_X_Scroll ;save as new value here to impose force on scroll + lda ScrollLock ;check scroll lock flag + bne InitScrlAmt ;skip a bunch of code here if set + lda Player_Pos_ForScroll + cmp #$50 ;check player's horizontal screen position + bcc InitScrlAmt ;if less than 80 pixels to the right, branch + lda SideCollisionTimer ;if timer related to player's side collision + bne InitScrlAmt ;not expired, branch + ldy Player_X_Scroll ;get value and decrement by one + ; dey ;if value originally set to zero or otherwise + ; bmi InitScrlAmt ;negative for left movement, branch + ; iny + cpy #$02 ;if value $01, branch and do not decrement + bcc ChkNearMid + dey ;otherwise decrement by one +ChkNearMid: + lda Player_Pos_ForScroll + cmp #$70 ;check player's horizontal screen position + bcc ScrollScreen ;if less than 112 pixels to the right, branch + ldy Player_X_Scroll ;otherwise get original value undecremented + ; fallthrough +ScrollScreen: + tya + sta ScrollAmount ;save value here + rts + ; clc + ; adc ScrollThirtyTwo ;add to value already set here + ; sta ScrollThirtyTwo ;save as new value here + ; tya + ; clc + ; adc ScreenLeft_X_Pos ;add to left side coordinate + ; sta ScreenLeft_X_Pos ;save as new left side coordinate + ; ; sta HorizontalScroll ;save here also + ; lda ScreenLeft_PageLoc + ; adc #$00 ;add carry to page location for left + ; sta ScreenLeft_PageLoc ;side of the screen + ; and #$01 ;get LSB of page location + ; sta R0 ;save as temp variable for PPU register 1 mirror + ; ; lda Mirror_PPUCTRL ;get PPU register 1 mirror + ; ; and #%11111110 ;save all bits except d0 + ; ; ora R0 ;get saved bit here and save in PPU register 1 + ; ; sta Mirror_PPUCTRL ;mirror to be used to set name table later + ; jsr GetScreenPosition ;figure out where the right side is + ; jmp ChkPOffscr ;skip this part +InitScrlAmt: + lda #$00 + sta ScrollAmount ;initialize value here +; ChkPOffscr: +; ldx #$00 ;set X for player offset +; jsr GetXOffscreenBits ;get horizontal offscreen bits for player +; sta R0 ;save them here +; ldy #$00 ;load default offset (left side) +; asl ;if d7 of offscreen bits are set, +; bcs KeepOnscr ;branch with default offset +; iny ;otherwise use different offset (right side) +; lda R0 +; and #%00100000 ;check offscreen bits for d5 set +; beq InitPlatScrl ;if not set, branch ahead of this part +; KeepOnscr: +; lda ScreenEdge_X_Pos,y ;get left or right side coordinate based on offset +; sec +; sbc X_SubtracterData,y ;subtract amount based on offset +; sta Player_X_Position ;store as player position to prevent movement further +; lda ScreenEdge_PageLoc,y ;get left or right page location based on offset +; sbc #$00 ;subtract borrow +; sta Player_PageLoc ;save as player's page location +; lda Left_Right_Buttons ;check saved controller bits +; cmp OffscrJoypadBitsData,y ;against bits based on offset +; beq InitPlatScrl ;if not equal, branch +; lda #$00 +; sta Player_X_Speed ;otherwise nullify horizontal speed of player +; InitPlatScrl: +; lda #$00 ;nullify platform force imposed on scroll +; sta Platform_X_Scroll + rts + +X_SubtracterData: + .byte $00, $10 + +OffscrJoypadBitsData: + .byte $01, $02 + +; ------------------------------------------------------------ +.reloc +ProcessPlayerAction: + lda Player_State ;get player's state + cmp #$03 + beq ActionClimbing ;if climbing, branch here + cmp #$02 + beq ActionFalling ;if falling, branch here + cmp #$01 + bne ProcOnGroundActs ;if not jumping, branch here + lda SwimmingFlag + bne ActionSwimming ;if swimming flag set, branch elsewhere + ldy #$06 ;load offset for crouching + lda CrouchingFlag ;get crouching flag + bne NonAnimatedActs ;if set, branch to get offset for graphics table + ldy #$00 ;otherwise load offset for jumping + jmp NonAnimatedActs ;go to get offset to graphics table + +ProcOnGroundActs: + ldy #$06 ;load offset for crouching + lda CrouchingFlag ;get crouching flag + bne NonAnimatedActs ;if set, branch to get offset for graphics table + ldy #$02 ;load offset for standing + lda Player_X_Speed ;check player's horizontal speed + ora Left_Right_Buttons ;and left/right controller bits + beq NonAnimatedActs ;if no speed or buttons pressed, use standing offset + lda Player_XSpeedAbsolute ;load walking/running speed + cmp #$09 + bcc ActionWalkRun ;if less than a certain amount, branch, too slow to skid + lda Player_MovingDir ;otherwise check to see if moving direction + and PlayerFacingDir ;and facing direction are the same + bne ActionWalkRun ;if moving direction = facing direction, branch, don't skid +; .if ::USE_SMB2J_FEATURES +; lda GameEngineSubroutine +; cmp #$09 ;if running the change size, fire flower, injure +; bcs NoSkidS ;or death game engine subroutines, skip this +; ; Ported from SMB2j +; lda #Sfx_Skid ;otherwise play skid sound +; sta NoiseSoundQueue +; NoSkidS: +; .endif + iny ;otherwise increment to skid offset ($03) + +NonAnimatedActs: + jsr GetGfxOffsetAdder ;do a sub here to get offset adder for graphics table + lda #$00 + sta PlayerAnimCtrl ;initialize animation frame control +; .if USE_LOOPING_ANIM_CYCLE +; lda #1 +; sta PlayerAnimDirection +; .endif + lda PlayerGfxTblOffsets,y ;load offset to graphics table using size as offset + rts + +ActionFalling: + ldy #$04 ;load offset for walking/running + jsr GetGfxOffsetAdder ;get offset to graphics table + jmp GetCurrentAnimOffset ;execute instructions for falling state + +ActionWalkRun: + ldy #$04 ;load offset for walking/running + jsr GetGfxOffsetAdder ;get offset to graphics table + jmp FourFrameExtent ;execute instructions for normal state + +ActionClimbing: + ldy #$05 ;load offset for climbing + lda Player_Y_Speed ;check player's vertical speed + beq NonAnimatedActs ;if no speed, branch, use offset as-is + jsr GetGfxOffsetAdder ;otherwise get offset for graphics table + jmp ThreeFrameExtent ;then skip ahead to more code + +ActionSwimming: + ldy #$01 ;load offset for swimming + jsr GetGfxOffsetAdder + lda JumpSwimTimer ;check jump/swim timer + ora PlayerAnimCtrl ;and animation frame control + bne FourFrameExtent ;if any one of these set, branch ahead + lda A_B_Buttons + asl ;check for A button pressed + bcs FourFrameExtent ;branch to same place if A button pressed + +GetCurrentAnimOffset: + lda PlayerAnimCtrl ;get animation frame control + jmp GetOffsetFromAnimCtrl ;jump to get proper offset to graphics table + +FourFrameExtent: + lda #$03 ;load upper extent for frame control + jmp AnimationControl ;jump to get offset and animate player object + +ThreeFrameExtent: + lda #$02 ;load upper extent for frame control for climbing + +AnimationControl: + sta R0 ;store upper extent here + jsr GetCurrentAnimOffset ;get proper offset to graphics table + pha ;save offset to stack + lda PlayerAnimTimer ;load animation frame timer + bne ExAnimC ;branch if not expired + lda PlayerAnimTimerSet ;get animation frame timer amount + sta PlayerAnimTimer ;and set timer accordingly +; .if USE_LOOPING_ANIM_CYCLE +; lda PlayerAnimDirection +; bmi @NegativeAnimCycle +; lda PlayerAnimCtrl +; clc ;add one to animation frame control +; adc PlayerAnimDirection +; cmp R0 ;compare to upper extent +; bcc SetAnimC ;if frame control + 1 < upper extent, use as next +; lda #$ff +; sta PlayerAnimDirection +; lda R0 ;otherwise initialize frame control +; sec +; sbc #2 +; jmp SetAnimC +; @NegativeAnimCycle: +; lda PlayerAnimCtrl +; clc ;add one to animation frame control +; adc PlayerAnimDirection +; bpl SetAnimC ;if frame control + 1 < upper extent, use as next +; lda #1 +; sta PlayerAnimDirection +; ; lda #$00 ;otherwise initialize frame control +; ; jmp SetAnimC +; .else + lda PlayerAnimCtrl + clc ;add one to animation frame control + adc #$01 + cmp R0 ;compare to upper extent + bcc SetAnimC ;if frame control + 1 < upper extent, use as next + lda #$00 ;otherwise initialize frame control +; .endif +SetAnimC: + sta PlayerAnimCtrl ;store as new animation frame control +ExAnimC: + pla ;get offset to graphics table from stack and leave + rts + +GetGfxOffsetAdder: + lda PlayerSize ;get player's size + beq SzOfs ;if player big, use current offset as-is + tya ;for big player + clc ;otherwise add eight bytes to offset + adc #$08 ;for small player + tay +SzOfs: + rts ;go back + +; ChkForPlayerAttrib: +; ldy PlayerOAMOffset ;get sprite data offset +; lda GameEngineSubroutine +; cmp #$0b ;if executing specific game engine routine, +; beq KilledAtt ;branch to change third and fourth row OAM attributes +; lda PlayerGfxOffset ;get graphics table offset +; cmp #$50 +; beq C_S_IGAtt ;if crouch offset, either standing offset, +; cmp #$b8 ;or intermediate growing offset, +; beq C_S_IGAtt ;go ahead and execute code to change +; cmp #$c0 ;fourth row OAM attributes only +; beq C_S_IGAtt +; cmp #$c8 +; bne ExPlyrAt ;if none of these, branch to leave +; KilledAtt: +; lda Sprite_Attributes+16,y +; and #%00111111 ;mask out horizontal and vertical flip bits +; sta Sprite_Attributes+16,y ;for third row sprites and save +; lda Sprite_Attributes+20,y +; and #%00111111 +; ora #%01000000 ;set horizontal flip bit for second +; sta Sprite_Attributes+20,y ;sprite in the third row +; C_S_IGAtt: +; lda Sprite_Attributes+24,y +; and #%00111111 ;mask out horizontal and vertical flip bits +; sta Sprite_Attributes+24,y ;for fourth row sprites and save +; lda Sprite_Attributes+28,y +; and #%00111111 +; ora #%01000000 ;set horizontal flip bit for second +; sta Sprite_Attributes+28,y ;sprite in the fourth row +ExPlyrAt: + rts ;leave + +;------------------------------------------------------------------------------------- +;$00 - used for downward force +;$01 - used for upward force +;$02 - used for maximum vertical speed +.reloc +MovePlayerVertically: + ldx #$00 ;set X for player offset + lda TimerControl + bne NoJSChk ;if master timer control set, branch ahead + lda JumpspringAnimCtrl ;otherwise check to see if jumpspring is animating + bne ExPlayerAttr ;branch to leave if so +NoJSChk: + lda VerticalForce ;dump vertical force + sta R0 + lda #$04 ;set maximum vertical speed here + jmp ImposeGravitySprObj ;then jump to move player vertically + + +GetPlayerColors: +; lda $076F ; Spell Status +; and #$10 ; check for Fire Spell (bit 4) +; beq + +; +; + + + ; ldx VRAM_Buffer1_Offset ;get current buffer offset + ; ldy #$00 +; lda CurrentPlayer ;check which player is on the screen +; beq ChkFiery +; ldy #$04 ;load offset for luigi +; ChkFiery: + +; TODO: reimplement fire check to use spell fire +; lda PlayerStatus ;check player status +; cmp #$02 +; bne StartClrGet ;if fiery, load alternate offset for fiery player +; ldy #$08 +; StartClrGet: + ; lda #$03 ;do four colors + ; sta R0 +ClrGetLoop: +; lda PlayerColors,y ;fetch player colors and store them +; sta VRAM_Buffer1+3,x ;in the buffer +; iny +; inx +; dec R0 +; bpl ClrGetLoop +; ldx VRAM_Buffer1_Offset ;load original offset from before +; ; ldy BackgroundColorCtrl ;if this value is four or greater, it will be set +; ; bne SetBGColor ;therefore use it as offset to background color +; ; ldy AreaType ;otherwise use area type bits from area offset as offset +; ; SetBGColor: +; ldy #0 ; TODO fix bg color write +; lda BackgroundColors,y ;to background color instead +; sta VRAM_Buffer1+3,x +; lda #$3f ;set for sprite palette address +; sta VRAM_Buffer1,x ;save to buffer +; lda #$10 +; sta VRAM_Buffer1+1,x +; lda #$04 ;write length byte to buffer +; sta VRAM_Buffer1+2,x +; ; lda #$00 ;now the null terminator +; lda #$ff ; z2 terminator +; sta VRAM_Buffer1+7,x +; txa ;move the buffer pointer ahead 7 bytes +; clc ;in case we want to write anything else later +; adc #$07 +; SetVRAMOffset: +; sta VRAM_Buffer1_Offset ;store as new vram buffer offset +ExPlayerAttr: + rts + +; BGColorCtrl_Addr: +; .byte $00, $09, $0a, $04 + +; BackgroundColors: +; .byte $22, $22, $0f, $0f ;used by area type if bg color ctrl not set +; .byte $0f, $22, $0f, $0f ;used by background color control if set + +; PlayerColors: +; .byte $22, $16, $27, $18 ;mario's colors +; .byte $22, $30, $27, $19 ;luigi's colors +; .byte $22, $37, $27, $16 ;fiery (used by both) + + +ImposeGravitySprObj: + sta R2 ;set maximum speed here + lda #$00 ;set value to move downwards + ; jmp ImposeGravity ;jump to the code that actually moves it +.proc ImposeGravity + + pha ;push value to stack + lda Player_YMoveForceFractional,x + clc ;add value in movement force to contents of dummy variable + adc SprObject_Y_MoveForce,x + sta Player_YMoveForceFractional,x + ldy #$00 ;set Y to zero by default + lda SprObject_Y_Speed,x ;get current vertical speed + bpl AlterYP ;if currently moving downwards, do not decrement Y + dey ;otherwise decrement Y +AlterYP: + sty R7 ;store Y here + adc SprObject_Y_Position,x ;add vertical position to vertical speed plus carry + sta SprObject_Y_Position,x ;store as new vertical position + lda SprObject_Y_HighPos,x + adc R7 ;add carry plus contents of $07 to vertical high byte + sta SprObject_Y_HighPos,x ;store as new vertical high byte + lda SprObject_Y_MoveForce,x + clc + adc R0 ;add downward movement amount to contents of $0433 + sta SprObject_Y_MoveForce,x + lda SprObject_Y_Speed,x ;add carry to vertical speed and store + adc #$00 + sta SprObject_Y_Speed,x + cmp R2 ;compare to maximum speed + bmi ChkUpM ;if less than preset value, skip this part + lda SprObject_Y_MoveForce,x + cmp #$80 ;if less positively than preset maximum, skip this part + bcc ChkUpM + lda R2 + sta SprObject_Y_Speed,x ;keep vertical speed within maximum value + lda #$00 + sta SprObject_Y_MoveForce,x ;clear fractional +ChkUpM: + pla ;get value from stack + beq ExVMove ;if set to zero, branch to leave + lda R2 + eor #%11111111 ;otherwise get two's compliment of maximum speed + tay + iny + sty R7 ;store two's compliment here + lda SprObject_Y_MoveForce,x + sec ;subtract upward movement amount from contents + sbc R1 ;of movement force, note that $01 is twice as large as $00, + sta SprObject_Y_MoveForce,x ;thus it effectively undoes add we did earlier + lda SprObject_Y_Speed,x + sbc #$00 ;subtract borrow from vertical speed and store + sta SprObject_Y_Speed,x + cmp R7 ;compare vertical speed to two's compliment + bpl ExVMove ;if less negatively than preset maximum, skip this part + lda SprObject_Y_MoveForce,x + cmp #$80 ;check if fractional part is above certain amount, + bcs ExVMove ;and if so, branch to leave + lda R7 + sta SprObject_Y_Speed,x ;keep vertical speed within maximum value + lda #$ff + sta SprObject_Y_MoveForce,x ;clear fractional +ExVMove: + rts ;leave! +.endproc + + + +;------------------------------------------------------------------------------------- +;$02 - modified y coordinate +;$03 - stores metatile involved in block buffer collisions +;$04 - comes in with offset to block buffer adder data, goes out with low nybble x/y coordinate +;$05 - modified x coordinate +;$06-$07 - block buffer address +.reloc +; BlockBufferChk_FBall: +; ldy #$1a ;set offset for block buffer adder data +; txa +; clc +; adc #$07 ;add seven bytes to use +; tax +; lda #$00 ;set A to return vertical coordinate +; BBChk_E: +; jsr BlockBufferCollision ;do collision detection subroutine for sprite object +; ; ldx ObjectOffset ;get object offset +; cmp #$00 ;check to see if object bumped into anything +; rts + +BlockBufferAdderData: + .byte (BlockBuffer_Big_X_Adder - BlockBuffer_X_Adder) + .byte (BlockBuffer_Swimming_X_Adder - BlockBuffer_X_Adder) + .byte (BlockBuffer_Small_X_Adder - BlockBuffer_X_Adder) + +; misc objects use hardcoded offsets +MISC_BLOCK_BUFFER_START = $16 + +; BLOCK_BUFFER_ADDER_NECK_OFFSET = BlockBufferNeck_X_Adder - BlockBuffer_X_Adder + +BlockBuffer_X_Adder: +; Added to the sprite position to get the location to check for tile collision +; head, foot l, r, side 1 2 3, 4 +BlockBuffer_Big_X_Adder: + .byte $08+8, $03+8, $0c+8, $02+8, $02+8, $0d+8, $0d+8 ; big +BlockBuffer_Swimming_X_Adder: + .byte $08+8, $03+8, $0c+8, $02+8, $02+8, $0d+8, $0d+8 ; swimming +BlockBuffer_Small_X_Adder: + .byte $08+8, $03+8, $0c+8, $02+8, $02+8, $0d+8, $0d+8 ; small/crouching +BlockBuffer_Misc_X_Adder: + .byte $08+8, $00+8, $10+8, $04+8, $14+8, $04+8, $04+8 ; misc + +BlockBuffer_Y_Adder: + .byte $04, $20, $20, $08, $18, $08, $18 ; big + .byte $02, $20, $20, $08, $18, $08, $18 ; swimming + .byte $12, $20, $20, $18, $18, $18, $18 ; small/crouching + .byte $18, $14, $14, $06, $06, $08, $10 ; misc + + +BlockBufferColli_Feet: + iny ;if branched here, increment to next set of adders +BlockBufferColli_Head: + lda #$00 ;set flag to return vertical coordinate + beq BlockBufferPlayerCollision ; Unconditional +BlockBufferColli_Side: + lda #$01 ;set flag to return horizontal coordinate +BlockBufferPlayerCollision: + ldx #$00 ;set offset for player object +BlockBufferCollision: + pha ;save contents of A to stack + sty R4 ;save contents of Y here + lda BlockBuffer_X_Adder,y ;add horizontal coordinate + clc ;of object to value obtained using Y as offset + adc SprObject_X_Position,x + ; extended to support 4 screens + php + lsr + sta R5 ;store here + plp + lda SprObject_PageLoc,x + adc #$00 ;add carry to page location + cmp #4 ; check if the number is either 4 or negative + bcs FailedToGetBlock + ; and #$01 ;get LSB, mask out all other bits + and #3 + lsr ;move to carry + ror + ora R5 ;get stored value + ror ;rotate carry to MSB of A + ; lsr ;and effectively move high nybble to + lsr ;lower, LSB which became MSB will be + lsr ;d4 at this point + jsr GetBlockBufferAddr ;get address of block buffer into $06, $07 + ldy R4 ;get old contents of Y + lda SprObject_Y_Position,x ;get vertical coordinate of object + clc + adc BlockBuffer_Y_Adder,y ;add it to value obtained using Y as offset + and #%11110000 ;mask out low nybble + sec + sbc #$20 ;subtract 32 pixels for the status bar + sta R2 ;store result here + tay ;use as offset for block buffer + lda (R6),y ;check current content of block buffer + sta R3 ;and store here + ldy R4 ;get old contents of Y again + pla ;pull A from stack + bne RetXC ;if A = 1, branch + lda SprObject_Y_Position,x ;if A = 0, load vertical coordinate + jmp RetYC ;and jump +RetXC: + lda SprObject_X_Position,x ;otherwise load horizontal coordinate +RetYC: + and #%00001111 ;and mask out high nybble + sta R4 ;store masked out result here + lda R3 ;get saved content of block buffer + rts ;and leave +FailedToGetBlock: + pla + lda #0 + sta R3 + rts + +; GetBlockBufferAddr: +; lda $4d ; load link mario's lo x byte +; sta $06 +; lda $3b ; load link mario's hi y byte +; tay +; lda $06 ; now divide by 4 +; lsr +; lsr +; lsr +; lsr +; clc +; adc CollisionBufferAddrLo,y ; add it to get the actual Address of the block +; sta $06 +; lda CollisionBufferAddrHi,y +; sta $07 +; rts +GetBlockBufferAddr: + pha ;take value of A, save + lsr ;move high nybble to low + lsr + lsr + lsr + tay ;use nybble as pointer to high byte + lda CollisionBufferAddrHi,y ;of indirect here + sta R7 + pla + and #%00001111 ;pull from stack, mask out high nybble + clc + adc CollisionBufferAddrLo,y ;add to low byte + sta R6 ;store here and leave + rts + +CollisionBufferAddrLo: +.byte <(COLLISION_TILES+$d0*0), <(COLLISION_TILES+$d0*1), <(COLLISION_TILES+$d0*2), <(COLLISION_TILES+$d0*3) +CollisionBufferAddrHi: +.byte >(COLLISION_TILES+$d0*0), >(COLLISION_TILES+$d0*1), >(COLLISION_TILES+$d0*2), >(COLLISION_TILES+$d0*3) +;------------------------------------------------------------------------------------- +;$06-$07 - used to store block buffer address used as indirect + +; BlockBufferAddr: +; .byte Block_Buffer_1, >Block_Buffer_2 + +; GetBlockBufferAddr: +; pha ;take value of A, save +; lsr ;move high nybble to low +; lsr +; lsr +; lsr +; tay ;use nybble as pointer to high byte +; lda BlockBufferAddr+2,y ;of indirect here +; sta R7 +; pla +; and #%00001111 ;pull from stack, mask out high nybble +; clc +; adc BlockBufferAddr,y ;add to low byte +; sta R6 ;store here and leave +; rts + + +;------------------------------------------------------------------------------------- + +; FireballBGCollision: +; lda Fireball_Y_Position,x ;check fireball's vertical coordinate +; cmp #$18 +; bcc ClearBounceFlag ;if within the status bar area of the screen, branch ahead +; jsr BlockBufferChk_FBall ;do fireball to background collision detection on bottom of it +; beq ClearBounceFlag ;if nothing underneath fireball, branch +; jsr ChkForNonSolids ;check for non-solid metatiles +; beq ClearBounceFlag ;branch if any found +; lda Fireball_Y_Speed,x ;if fireball's vertical speed set to move upwards, +; bmi InitFireballExplode ;branch to set exploding bit in fireball's state +; lda FireballBouncingFlag,x ;if bouncing flag already set, +; bne InitFireballExplode ;branch to set exploding bit in fireball's state +; lda #$fd +; sta Fireball_Y_Speed,x ;otherwise set vertical speed to move upwards (give it bounce) +; lda #$01 +; sta FireballBouncingFlag,x ;set bouncing flag +; lda Fireball_Y_Position,x +; and #$f8 ;modify vertical coordinate to land it properly +; sta Fireball_Y_Position,x ;store as new vertical coordinate +; rts ;leave + +; ClearBounceFlag: +; lda #$00 +; sta FireballBouncingFlag,x ;clear bouncing flag by default +; rts ;leave + +; InitFireballExplode: +; lda #$80 +; sta Fireball_State,x ;set exploding flag in fireball's state +; lda #Sfx_Bump +; sta Square1SoundQueue ;load bump sound +; rts ;leave + +ChkForNonSolids: + cmp #$26 ;blank metatile used for vines? + beq NSFnd + cmp #$c2 ;regular coin? + beq NSFnd + cmp #$c3 ;underwater coin? + beq NSFnd + cmp #$5f ;hidden coin block? + beq NSFnd + cmp #$60 ;hidden 1-up block? +NSFnd: rts + + +;------------------------------------------------------------------------------------- +;$00-$01 - used to hold many values, essentially temp variables +;$04 - holds lower nybble of vertical coordinate from block buffer routine +;$eb - used to hold block buffer adder +.reloc +PlayerBGUpperExtent: + .byte $20, $10 + +PlayerBGCollision: + lda DisableCollisionDet ;if collision detection disabled flag set, + bne ExPBGCol ;branch to leave + ; clear the player metatile we are standing on + sta PlayerStandingMetatile + lda GameEngineSubroutine + cmp #$0b ;if running routine #11 or $0b + beq ExPBGCol ;branch to leave + cmp #$04 + bcc ExPBGCol ;if running routines $00-$03 branch to leave + lda #$01 ;load default player state for swimming + ldy SwimmingFlag ;if swimming flag set, + bne SetPSte ;branch ahead to set default state + lda Player_State ;if player in normal state, + beq SetFallS ;branch to set default state for falling + cmp #$03 + bne ChkOnScr ;if in any other state besides climbing, skip to next part +SetFallS: + lda #$02 ;load default player state for falling +SetPSte: + + ldy $0754 ; zelda check for elevator state + bne ChkOnScr + sta Player_State ;set whatever player state is appropriate +ChkOnScr: + lda Player_Y_HighPos + cmp #$01 ;check player's vertical high byte for still on the screen + bne ExPBGCol ;branch to leave if not + lda #$ff + sta Player_CollisionBits ;initialize player's collision flag + lda Player_Y_Position + cmp #$cf ;check player's vertical coordinate + bcc ChkCollSize ;if not too close to the bottom of screen, continue +ExPBGCol: + rts ;otherwise leave + +ChkCollSize: + ldy #$02 ;load default offset + lda CrouchingFlag + bne GBBAdr ;if player crouching, skip ahead + lda PlayerSize + bne GBBAdr ;if player small, skip ahead + dey ;otherwise decrement offset for big player not crouching + lda SwimmingFlag + bne GBBAdr ;if swimming flag set, skip ahead + dey ;otherwise decrement offset +GBBAdr: + lda BlockBufferAdderData,y ;get value using offset + sta Local_eb ;store value here + tay ;put value into Y, as offset for block buffer routine + ldx PlayerSize ;get player's size as offset + lda CrouchingFlag + beq HeadChk ;if player not crouching, branch ahead + inx ;otherwise increment size as offset +HeadChk: + lda Player_Y_Position ;get player's vertical coordinate + cmp PlayerBGUpperExtent,x ;compare with upper extent value based on offset + bcc DoFootCheck ;if player is too high, skip this part + jsr BlockBufferColli_Head ;do player-to-bg collision detection on top of + beq DoFootCheck ;player, and branch if nothing above player's head + ; jsr CheckForCoinMTiles ;check to see if player touched coin with their head + ; bcs AwardTouchedCoin ;if so, branch to some other part of code + ldy Player_Y_Speed ;check player's vertical speed + bpl DoFootCheck ;if player not moving upwards, branch elsewhere + ldy R4 ;check lower nybble of vertical coordinate returned + cpy #$04 ;from collision detection routine + bcc DoFootCheck ;if low nybble < 4, branch + ; we can't check solid type here since its banked out + ; jsr CheckForSolidMTiles ;check to see what player's head bumped on + bcs SolidOrClimb ;if player collided with solid metatile, branch + ldy AreaType ;otherwise check area type + beq NYSpd ;if water level, branch ahead + ldy BlockBounceTimer ;if block bounce timer not expired, + bne NYSpd ;branch ahead, do not process collision + jsr PlayerHeadCollision ;otherwise do a sub to process collision + jmp DoFootCheck ;jump ahead to skip these other parts + +SolidOrClimb: + ; cmp #$26 ;if climbing metatile, + ; beq NYSpd ;branch ahead and do not play sound + lda #Sfx_Bump + sta Square1SoundQueue ;otherwise load bump sound +NYSpd: + lda #$01 ;set player's vertical speed to nullify + sta Player_Y_Speed ;jump or swim + +DoFootCheck: + ldy Local_eb ;get block buffer adder offset + lda Player_Y_Position + cmp #$cf ;check to see how low player is + bcs DoPlayerSideCheck ;if player is too far down on screen, skip all of this + jsr BlockBufferColli_Feet ;do player-to-bg collision detection on bottom left of player + ; jsr CheckForCoinMTiles ;check to see if player touched coin with their left foot + ; bcs AwardTouchedCoin ;if so, branch to some other part of code + pha ;save bottom left metatile to stack + jsr BlockBufferColli_Feet ;do player-to-bg collision detection on bottom right of player + sta R0 ;save bottom right metatile here + pla + sta R1 ;pull bottom left metatile and save here + bne ChkFootMTile ;if anything here, skip this part + lda R0 ;otherwise check for anything in bottom right metatile + beq DoPlayerSideCheck ;and skip ahead if not + ; jsr CheckForCoinMTiles ;check to see if player touched coin with their right foot + ; bcc ChkFootMTile ;if not, skip unconditional jump and continue code +AwardTouchedCoin: + ; jmp HandleCoinMetatile ;follow the code to erase coin and award to player 1 coin + ;implicit rts + +ChkFootMTile: + ; jsr CheckForClimbMTiles ;check to see if player landed on climbable metatiles + ; bcs DoPlayerSideCheck ;if so, branch + ldy Player_Y_Speed ;check player's vertical speed + bmi DoPlayerSideCheck ;if player moving upwards, branch + ; cmp #$c5 + ; bne ContChk ;if player did not touch axe, skip ahead + ; jmp HandleAxeMetatile ;otherwise jump to set modes of operation +ContChk: + ; jsr ChkInvisibleMTiles ;do sub to check for hidden coin or 1-up blocks + ; beq DoPlayerSideCheck ;if either found, branch + ; ldy JumpspringAnimCtrl ;if jumpspring animating right now, + ; bne InitSteP ;branch ahead + ldy R4 ;check lower nybble of vertical coordinate returned + cpy #$05 ;from collision detection routine + bcc LandPlyr ;if lower nybble < 5, branch + lda Player_MovingDir + sta R0 ;use player's moving direction as temp variable + jmp StopPlayerMove ;jump to impede player's movement in that direction +LandPlyr: + ; jsr ChkForLandJumpSpring ;do sub to check for jumpspring metatiles and deal with it + lda #$f0 + and Player_Y_Position ;mask out lower nybble of player's vertical position + sta Player_Y_Position ;and store as new vertical position to land player properly + ; jsr HandlePipeEntry ;do sub to process potential pipe entry + lda #$00 + sta Player_Y_Speed ;initialize vertical speed and fractional + sta Player_Y_MoveForce ;movement force to stop player's vertical movement + ; sta StompChainCounter ;initialize enemy stomp counter +; cancel downstab hitbox?? + lda #$f8 + sta $480 + lda $cc + clc + adc #$10 + sta $47e +InitSteP: + lda #$00 + sta Player_State ;set player's state to normal + ; fallthrough + +DoPlayerSideCheck: + ldy Local_eb ;get block buffer adder offset + iny + iny ;increment offset 2 bytes to use adders for side collisions + lda #2 + sta R0 +SideCheckLoop: + iny ;move onto the next one + sty Local_eb ;store it + lda Player_Y_Position + cmp #$20 ;check player's vertical position + bcc BHalf ;if player is in status bar area, branch ahead to skip this part + cmp #$e4 + bcs ExSCH ;branch to leave if player is too far down + jsr BlockBufferColli_Side ;do player-to-bg collision detection on one half of player + bne CheckSideMTiles ;branch if found something + ; beq BHalf ;branch ahead if nothing found + ; cmp #$1c ;otherwise check for pipe metatiles + ; beq BHalf ;if collided with sideways pipe (top), branch ahead + ; cmp #$6b + ; beq BHalf ;if collided with water pipe (top), branch ahead + ; jsr CheckForClimbMTiles ;do sub to see if player bumped into anything climbable + ; bcc CheckSideMTiles ;if not, branch to alternate section of code +BHalf: + ldy Local_eb ;load block adder offset + iny ;increment it + lda Player_Y_Position ;get player's vertical position + cmp #$08 + bcc ExSCH ;if too high, branch to leave + cmp #$d0 + bcs ExSCH ;if too low, branch to leave + jsr BlockBufferColli_Side ;do player-to-bg collision detection on other half of player + bne CheckSideMTiles ;if something found, branch + dec R0 ;otherwise decrement counter + bne SideCheckLoop ;run code until both sides of player are checked +ExSCH: + rts ;leave + +CheckSideMTiles: + ; for now just always block movement + jmp StopPlayerMove + + ; jsr ChkInvisibleMTiles ;check for hidden or coin 1-up blocks + ; beq ExSCH ;branch to leave if either found + ; jsr CheckForClimbMTiles ;check for climbable metatiles + ; bcc ContSChk ;if not found, skip and continue with code + ; jmp HandleClimbing ;otherwise jump to handle climbing +ContSChk: + ; jsr CheckForCoinMTiles ;check to see if player touched coin + ; bcs HandleCoinMetatile ;if so, execute code to erase coin and award to player 1 coin + ; jsr ChkJumpspringMetatiles ;check for jumpspring metatiles + ; bcc ChkPBtm ;if not found, branch ahead to continue cude + ; lda JumpspringAnimCtrl ;otherwise check jumpspring animation control + ; bne ExSCH ;branch to leave if set + ; jmp StopPlayerMove ;otherwise jump to impede player's movement +ChkPBtm: + ; ldy Player_State ;get player's state + ; cpy #$00 ;check for player's state set to normal + ; bne StopPlayerMove ;if not, branch to impede player's movement + ; ldy PlayerFacingDir ;get player's facing direction + ; dey + ; bne StopPlayerMove ;if facing left, branch to impede movement +; cmp #$6c ;otherwise check for pipe metatiles +; beq PipeDwnS ;if collided with sideways pipe (bottom), branch +; cmp #$1f ;if collided with water pipe (bottom), continue +; bne StopPlayerMove ;otherwise branch to impede player's movement +; PipeDwnS: + ; lda Player_SprAttrib ;check player's attributes + ; bne PlyrPipe ;if already set, branch, do not play sound again + ; lda InPipeTransition + ; bne :+ + ; ldy #Sfx_PipeDown_Injury + ; sty Square1SoundQueue ;otherwise load pipedown/injury sound + ; lda PlayerEntranceCtrl ;;; check if we are entering the pipe in auto mode + ; cmp #7 + ; bne :+ + ;; if we are then start a pipe transition + ; .import SetupPipeTransitionOverlay + ; lda #2 + ; jsr SetupPipeTransitionOverlay + ; : +; PlyrPipe: +; ora #%00100000 +; sta Player_SprAttrib ;set background priority bit in player attributes +; lda Player_X_Position +; and #%00001111 ;get lower nybble of player's horizontal coordinate +; beq ChkGERtn ;if at zero, branch ahead to skip this part +; ldy #$00 ;set default offset for timer setting data +; lda ScreenLeft_PageLoc ;load page location for left side of screen +; beq SetCATmr ;if at page zero, use default offset +; iny ;otherwise increment offset +; SetCATmr: +; lda AreaChangeTimerData,y ;set timer for change of area as appropriate +; sta ChangeAreaTimer +; ChkGERtn: +; lda GameEngineSubroutine ;get number of game engine routine running +; cmp #$07 +; beq ExCSM ;if running player entrance routine or +; cmp #$08 ;player control routine, go ahead and branch to leave +; bne ExCSM +; lda #$02 +; sta GameEngineSubroutine ;otherwise set sideways pipe entry routine to run +ExCSM: + rts ;and leave + +;-------------------------------- +;$02 - high nybble of vertical coordinate from block buffer +;$04 - low nybble of horizontal coordinate from block buffer +;$06-$07 - block buffer address + +StopPlayerMove: + jmp ImpedePlayerMove ;stop player's movement + +.reloc +ImpedePlayerMove: + lda #$00 ;initialize value here + ldy Player_X_Speed ;get player's horizontal speed + ldx R0 ;check value set earlier for + dex ;left side collision + bne RImpd ;if right side collision, skip this part + inx ;return value to X + cpy #$00 ;if player moving to the left, + bmi ExIPM ;branch to invert bit and leave + lda #$ff ;otherwise load A with value to be used later + bne NXSpd ;and jump to affect movement +RImpd: + ldx #$02 ;return $02 to X + cpy #$01 ;if player moving to the right, + bpl ExIPM ;branch to invert bit and leave + lda #$01 ;otherwise load A with value to be used here +NXSpd: + ldy #$10 + sty SideCollisionTimer ;set timer of some sort + ldy #$00 + sty Player_X_Speed ;nullify player's horizontal speed + cmp #$00 ;if value set in A not set to $ff, + bpl PlatF ;branch ahead, do not decrement Y + dey ;otherwise decrement Y now +PlatF: + sty R0 ;store Y as high bits of horizontal adder + clc + adc Player_X_Position ;add contents of A to player's horizontal + sta Player_X_Position ;position to move player left or right + lda Player_PageLoc + adc R0 ;add high bits and carry to + sta Player_PageLoc ;page location if necessary +ExIPM: + txa ;invert contents of X + eor #$ff + and Player_CollisionBits ;mask out bit that was set here + sta Player_CollisionBits ;store to clear bit + rts + + +;------------------------------------------------------------------------------------- +;These apply to all routines in this section unless otherwise noted: +;$00 - used to store metatile from block buffer routine +;$02 - used to store vertical high nybble offset from block buffer routine +;$05 - used to store metatile stored in A at beginning of PlayerHeadCollision +;$06-$07 - used as block buffer address indirect +.reloc +BlockYPosAdderData: +; Big, Small + .byte $04, $12 + +PlayerHeadCollision: + ; pha ;store metatile number to stack +; lda #$11 ;load unbreakable block object state by default +; ldx SprDataOffset_Ctrl ;load offset control bit here +; ldy PlayerSize ;check player's size +; bne :+ ;if small, branch +; lda #$12 ;otherwise load breakable block object state +; : +; sta Block_State,x ;store into block object buffer + ; farcall DestroyBlockMetatile ;store blank metatile in vram buffer to write to name table + ; ldx SprDataOffset_Ctrl ;load offset control bit + ; lda R2 ;get vertical high nybble offset used in block buffer routine + ; sta Block_Orig_YPos,x ;set as vertical coordinate for block object + ; tay + ; lda R6 ;get low byte of block buffer address used in same routine + ; sta Block_BBuf_Low,x ;save as offset here to be used later + ; lda (R6),y ;get contents of block buffer at old address at $06, $07 + ; jsr BlockBumpedChk ;do a sub to check which block player bumped head on + ; sta R0 ;store metatile here +; ldy PlayerSize ;check player's size +; bne :+ ;if small, use metatile itself as contents of A +; tya ;otherwise init A (note: big = 0) +; : +; bcc PutMTileB ;if no match was found in previous sub, skip ahead +; ; ldy #$11 ;otherwise load unbreakable state into block object buffer +; ; sty Block_State,x ;note this applies to both player sizes +; lda #$c4 ;load empty block metatile into A for now +; ldy R0 ;get metatile from before +; cpy #$58 ;is it brick with coins (with line)? +; beq StartBTmr ;if so, branch +; cpy #$5d ;is it brick with coins (without line)? +; bne PutMTileB ;if not, branch ahead to store empty block metatile +; StartBTmr: +; lda BrickCoinTimerFlag ;check brick coin timer flag +; bne :+ ;if set, timer expired or counting down, thus branch +; lda #$0b +; sta BrickCoinTimer ;if not set, set brick coin timer +; inc BrickCoinTimerFlag ;and set flag linked to it +; : +; lda BrickCoinTimer ;check brick coin timer +; bne :+ ;if not yet expired, branch to use current metatile +; ldy #$c4 ;otherwise use empty block metatile +; : +; tya ;put metatile into A +; PutMTileB: +; sta Block_Metatile,x ;store whatever metatile be appropriate here +; jsr InitBlock_XY_Pos ;get block object horizontal coordinates saved +; ldy R2 ;get vertical high nybble offset +; lda #$23 +; sta (R6),y ;write blank metatile $23 to block buffer +; lda #$10 +; sta BlockBounceTimer ;set block bounce timer +; pla ;pull original metatile from stack +; sta R5 ;and save here +; ldy #$00 ;set default offset +; lda CrouchingFlag ;is player crouching? +; bne SmallBP ;if so, branch to increment offset +; lda PlayerSize ;is player big? +; beq BigBP ;if so, branch to use default offset +; SmallBP: +; iny ;increment for small or big and crouching +; BigBP: +; lda Player_Y_Position ;get player's vertical coordinate +; clc +; adc BlockYPosAdderData,y ;add value determined by size +; and #$f0 ;mask out low nybble to get 16-pixel correspondence +; sta Block_Y_Position,x ;save as vertical coordinate for block object +; ldy Block_State,x ;get block object state +; cpy #$11 +; beq :+ ;if set to value loaded for unbreakable, branch +; jsr BrickShatter ;execute code for breakable brick +; jmp InvOBit ;skip subroutine to do last part of code here +; : +; jsr BumpBlock ;execute code for unbreakable brick or question block +; InvOBit: + ; lda SprDataOffset_Ctrl ;invert control bit used by block objects + ; eor #$01 ;and floatey numbers + ; sta SprDataOffset_Ctrl + + + lda #$00 + sta Player_Y_Speed ;init player's vertical speed + + rts ;leave! + +ProcFireball_Bubble: +; lda PlayerStatus ;check player's status +; cmp #$02 +; bcc ProcAirBubbles ;if not fiery, branch + lda A_B_Buttons + and #B_Button ;check for b button pressed + beq ProcFireballs ;branch if not pressed + and PreviousA_B_Buttons + bne ProcFireballs ;if button pressed in previous frame, branch + lda FireballCounter ;load fireball counter + and #%00000001 ;get LSB and use as offset for buffer + tax + lda Fireball_State,x ;load fireball state + bne ProcFireballs ;if not inactive, branch + ldy Player_Y_HighPos ;if player too high or too low, branch + dey + bne ProcFireballs + lda CrouchingFlag ;if player crouching, branch + bne ProcFireballs + lda Player_State ;if player's state = climbing, branch + cmp #$03 + beq ProcFireballs + lda #Sfx_Fireball ;play fireball sound effect + sta Square1SoundQueue + lda #$02 ;load state + sta Fireball_State,x + ldy PlayerAnimTimerSet ;copy animation frame timer setting + sty FireballThrowingTimer ;into fireball throwing timer + dey + sty PlayerAnimTimer ;decrement and store in player's animation timer + inc FireballCounter ;increment fireball counter + +ProcFireballs: + ldx #$00 + jsr FireballObjCore ;process first fireball object + ldx #$01 + jmp FireballObjCore ;process second fireball object, then do air bubbles + +FireballXSpdData: + .byte 64 + .byte -64 + +FireballObjCore: + stx ObjectOffset ;store offset as current object + lda Fireball_State,x ;check for d7 = 1 + asl + bcs FireballExplosion ;if so, branch to get relative coordinates and draw explosion + ldy Fireball_State,x ;if fireball inactive, branch to leave + beq NoFBall + dey ;if fireball state set to 1, skip this part and just run it + beq RunFB + lda Player_X_Position ;get player's horizontal position + adc #$04 ;add four pixels and store as fireball's horizontal position + sta Fireball_X_Position,x + lda Player_PageLoc ;get player's page location + adc #$00 ;add carry and store as fireball's page location + sta Fireball_PageLoc,x + lda Player_Y_Position ;get player's vertical position and store + sta Fireball_Y_Position,x + lda #$01 ;set high byte of vertical position + sta Fireball_Y_HighPos,x + ldy PlayerFacingDir ;get player's facing direction + dey ;decrement to use as offset here + lda FireballXSpdData,y ;set horizontal speed of fireball accordingly + sta Fireball_X_Speed,x + lda #4 ;set vertical speed of fireball + sta Fireball_Y_Speed,x +; lda #$07 +; sta Fireball_BoundBoxCtrl,x ;set bounding box size control for fireball + dec Fireball_State,x ;decrement state to 1 to skip this part from now on +RunFB: + txa ;add 7 to offset to use + clc ;as fireball offset for next routines + adc #FireballOffset + tax + lda #$50 ;set downward movement force here + sta $00 + lda #3 ;set maximum speed here + sta $02 + lda #$00 + jsr ImposeGravity ;do sub here to impose gravity on fireball and move vertically + jsr MoveObjectHorizontally ;do another sub to move it horizontally + + ldy #3 + jsr $F27D ; run the vanilla z2 offscreen check + + ldx ObjectOffset ;return fireball offset to X + ; jsr RelativeFireballPosition ;get relative coordinates + ; jsr GetFireballOffscreenBits ;get offscreen information + ; jsr GetFireballBoundBox ;get bounding box coordinates + jsr FireballBGCollision ;do fireball to background collision detection + + ; load the offscreen bits for this enemy + lda $cb + and #$FC + + ; lda FBall_OffscreenBits ;get fireball offscreen bits + ; and #%11001100 ;mask out certain bits + bne EraseFB ;if any bits still set, branch to kill fireball +; jsr FireballEnemyCollision ;do fireball to enemy collision detection and deal with collisions + jmp DrawFireball ;draw fireball appropriately and leave +EraseFB: + lda #$00 ;erase fireball state + sta Fireball_State,x + sta FireballMetasprite,x +NoFBall: + rts ;leave + +FireballExplosion: + jsr RelativeFireballPosition + jmp DrawExplosion_Fireball + +RelativeFireballPosition: + ldy #$00 ;set for fireball offsets + jsr GetProperObjOffset ;modify X to get proper fireball offset + ldy #$02 +RelWOfs: +; jsr GetObjRelativePosition ;get the coordinates + ldx ObjectOffset ;return original offset + rts ;leave + +; GetFireballOffscreenBits: +; ldy #$00 ;set for fireball offsets +; jsr GetProperObjOffset ;modify X to get proper fireball offset +; ldy #$02 ;set other offset for fireball's offscreen bits +; jmp GetOffScreenBitsSet ;and get offscreen information about fireball + +; GetFireballBoundBox: +; txa ;add seven bytes to offset +; clc ;to use in routines as offset for fireball +; adc #$07 +; tax +; ldy #$02 ;set offset for relative coordinates +; bne FBallB ;unconditional branch + +; GetMiscBoundBox: +; txa ;add nine bytes to offset +; clc ;to use in routines as offset for misc object +; adc #$09 +; tax +; ldy #$06 ;set offset for relative coordinates +; FBallB: +; jsr BoundingBoxCore ;get bounding box coordinates +; jmp CheckRightScreenBBox ;jump to handle any offscreen coordinates + +FireballBGCollision: + lda Fireball_Y_Position,x ;check fireball's vertical coordinate + cmp #$18 + bcc ClearBounceFlag ;if within the status bar area of the screen, branch ahead + jsr BlockBufferChk_FBall ;do fireball to background collision detection on bottom of it + beq ClearBounceFlag ;if nothing underneath fireball, branch + jsr ChkForNonSolids ;check for non-solid metatiles + beq ClearBounceFlag ;branch if any found + lda Fireball_Y_Speed,x ;if fireball's vertical speed set to move upwards, + bmi InitFireballExplode ;branch to set exploding bit in fireball's state + lda FireballBouncingFlag,x ;if bouncing flag already set, + bne InitFireballExplode ;branch to set exploding bit in fireball's state + lda #-3 + sta Fireball_Y_Speed,x ;otherwise set vertical speed to move upwards (give it bounce) + lda #$01 + sta FireballBouncingFlag,x ;set bouncing flag + lda Fireball_Y_Position,x + and #$f8 ;modify vertical coordinate to land it properly + sta Fireball_Y_Position,x ;store as new vertical coordinate + rts ;leave + +ClearBounceFlag: + lda #$00 + sta FireballBouncingFlag,x ;clear bouncing flag by default + rts ;leave + +InitFireballExplode: + lda #$80 + sta Fireball_State,x ;set exploding flag in fireball's state + lda #Sfx_Bump + sta Square1SoundQueue ;load bump sound + rts ;leave + +;FireballEnemyCollision: +; lda Fireball_State,x ;check to see if fireball state is set at all +; beq ExitFBallEnemy ;branch to leave if not +; asl +; bcs ExitFBallEnemy ;branch to leave also if d7 in state is set +; lda FrameCounter +; lsr ;get LSB of frame counter +; bcs ExitFBallEnemy ;branch to leave if set (do routine every other frame) +; txa +; asl ;multiply fireball offset by four +; asl +; clc +; adc #$1c ;then add $1c or 28 bytes to it +; tay ;to use fireball's bounding box coordinates +; ldx #$04 +; +;FireballEnemyCDLoop: +; stx $01 ;store enemy object offset here +; tya +; pha ;push fireball offset to the stack +; lda Enemy_State,x +; and #%00100000 ;check to see if d5 is set in enemy state +; bne NoFToECol ;if so, skip to next enemy slot +; lda Enemy_Flag,x ;check to see if buffer flag is set +; beq NoFToECol ;if not, skip to next enemy slot +; lda Enemy_ID,x ;check enemy identifier +; cmp #$24 +; bcc GoombaDie ;if < $24, branch to check further +; cmp #$2b +; bcc NoFToECol ;if in range $24-$2a, skip to next enemy slot +;GoombaDie: +; cmp #Goomba ;check for goomba identifier +; bne NotGoomba ;if not found, continue with code +; lda Enemy_State,x ;otherwise check for defeated state +; cmp #$02 ;if stomped or otherwise defeated, +; bcs NoFToECol ;skip to next enemy slot +;NotGoomba: +; lda EnemyOffscrBitsMasked,x ;if any masked offscreen bits set, +; bne NoFToECol ;skip to next enemy slot +; txa +; asl ;otherwise multiply enemy offset by four +; asl +; clc +; adc #$04 ;add 4 bytes to it +; tax ;to use enemy's bounding box coordinates +; jsr SprObjectCollisionCore ;do fireball-to-enemy collision detection +; ldx ObjectOffset ;return fireball's original offset +; bcc NoFToECol ;if carry clear, no collision, thus do next enemy slot +; lda #%10000000 +; sta Fireball_State,x ;set d7 in enemy state +; ldx $01 ;get enemy offset +; jsr HandleEnemyFBallCol ;jump to handle fireball to enemy collision +;NoFToECol: +; pla ;pull fireball offset from stack +; tay ;put it in Y +; ldx $01 ;get enemy object offset +; dex ;decrement it +; bpl FireballEnemyCDLoop ;loop back until collision detection done on all enemies +; +;ExitFBallEnemy: +; ldx ObjectOffset ;get original fireball offset and leave +; rts + +DrawFireball: + lda FrameCounter ;get frame counter + lsr ;divide by four + lsr + pha ;save result to stack + ;and #$01 ;mask out all but last bit + ; eor #FIREBALL_TILE1 ;set either tile $64 or $65 as fireball tile + ; sta Sprite_Tilenumber,y ;thus tile changes every four frames + lsr + lda #METASPRITE_FIREBALL_FRAME_1 + bcc :+ + lda #METASPRITE_FIREBALL_FRAME_2 + : + sta FireballMetasprite,x + ; sta R0 + pla ;get from stack + lsr ;divide by four again + lsr + lda #$00 ;load value $02 to set palette in attrib byte + bcc FireA ;if last bit shifted out was not set, skip this + ora #%11000000 ;otherwise flip both ways every eight frames +FireA: + sta Fireball_SprAttrib,x ;store attribute byte and leave + rts +; jsr GetFBOAMOffset +; ldx R0 +; jmp DrawMetasprite + +; GetFBOAMOffset: +; lda ObjectOffset +; clc +; adc #14 +; tay +; rts + +ExplosionTiles: + .byte METASPRITE_EXPLOSION_FRAME_1 + .byte METASPRITE_EXPLOSION_FRAME_2 + .byte METASPRITE_EXPLOSION_FRAME_3 + +DrawExplosion_Fireball: + ; ldy Alt_SprDataOffset,x ;get OAM data offset of alternate sort for fireball's explosion + lda Fireball_State,x ;load fireball state + inc Fireball_State,x ;increment state for next frame + lsr ;divide by 2 + and #%00000111 ;mask out all but d3-d1 + cmp #$03 ;check to see if time to kill fireball + bcs KillFireBall ;branch if so, otherwise continue to draw explosion + ;fallthrough + tay ;use whatever's in A for offset + ; prevent rotation of the fireball from bleeding into the explosion +; lda #0 +; sta Fireball_SprAttrib,x + ; lda ExplosionTiles,y ;get tile number using offset + ; sta R0 + rts + ; jsr GetFBOAMOffset + ; ldx R0 + ; jmp DrawMetasprite + +KillFireBall: + lda #$00 ;clear fireball state to kill it + sta FireballMetasprite,x + sta Fireball_State,x + rts + ; jsr GetFBOAMOffset + ; ldx R0 + ; jmp DrawMetasprite + +ObjOffsetData: + .byte $07, $16, $0d + +GetProperObjOffset: + txa ;move offset to A + clc + adc ObjOffsetData,y ;add amount of bytes to offset depending on setting in Y + tax ;put back in X and leave + rts + +; .proc GetXOffscreenBits +; stx R4 ;save position in buffer to here +; ldy #$01 ;start with right side of screen +; Loop: +; lda ScreenEdge_X_Pos,y ;get pixel coordinate of edge +; sec ;get difference between pixel coordinate of edge +; sbc SprObject_X_Position,x ;and pixel coordinate of object position +; sta R7 ;store here +; lda ScreenEdge_PageLoc,y ;get page location of edge +; sbc SprObject_PageLoc,x ;subtract page location of object position from it +; ldx DefaultXOnscreenOfs,y ;load offset value here +; cmp #$00 +; bmi Continue ;if beyond right edge or in front of left edge, branch +; ldx DefaultXOnscreenOfs+1,y ;if not, load alternate offset value here +; cmp #$01 +; bpl Continue ;if one page or more to the left of either edge, branch +; lda #$38 ;if no branching, load value here and store +; sta R6 +; lda #$08 ;load some other value and execute subroutine +; ; jsr DividePDiff ; inlined +; sta R5 ;store current value in A here +; lda R7 ;get pixel difference +; cmp R6 ;compare to preset value +; bcs Continue ;if pixel difference >= preset value, branch +; lsr ;divide by eight to get tile difference +; lsr +; lsr +; and #$07 ;mask out all but 3 LSB +; cpy #$01 ;right side of the screen or top? +; bcs + ;if so, branch, use difference / 8 as offset +; adc R5 ;if not, add value to difference / 8 +; + +; tax ;use as offset +; Continue: +; lda XOffscreenBitsData,x ;get bits here +; ldx R4 ;reobtain position in buffer +; cmp #$00 ;if bits not zero, branch to leave +; bne ExXOfsBS +; dey ;otherwise, do left side of screen now +; bpl Loop ;branch if not already done with left side +; ExXOfsBS: +; rts + +; XOffscreenBitsData: +; .byte $7f, $3f, $1f, $0f, $07, $03, $01, $00 +; .byte $80, $c0, $e0, $f0, $f8, $fc, $fe, $ff + +; DefaultXOnscreenOfs: +; .byte $07, $0f, $07 +; .endproc + +; .proc GetYOffscreenBits +; stx R4 ;save position in buffer to here +; ldy #$01 ;start with top of screen +; Loop: +; lda HighPosUnitData,y ;load coordinate for edge of vertical unit +; sec +; sbc SprObject_Y_Position,x ;subtract from vertical coordinate of object +; sta R7 ;store here +; lda #$01 ;subtract one from vertical high byte of object +; sbc SprObject_Y_HighPos,x +; ldx DefaultYOnscreenOfs,y ;load offset value here +; cmp #$00 +; bmi Continue ;if under top of the screen or beyond bottom, branch +; ldx DefaultYOnscreenOfs+1,y ;if not, load alternate offset value here +; cmp #$01 +; bpl Continue ;if one vertical unit or more above the screen, branch +; lda #$20 ;if no branching, load value here and store +; sta R6 +; lda #$04 ;load some other value and execute subroutine +; ; jsr DividePDiff ; inlined +; sta R5 ;store current value in A here +; lda R7 ;get pixel difference +; cmp R6 ;compare to preset value +; bcs Continue ;if pixel difference >= preset value, branch +; lsr ;divide by eight to get tile difference +; lsr +; lsr +; and #$07 ;mask out all but 3 LSB +; cpy #$01 ;right side of the screen or top? +; bcs + ;if so, branch, use difference / 8 as offset +; adc R5 ;if not, add value to difference / 8 +; + +; tax ;use as offset +; Continue: +; lda YOffscreenBitsData,x ;get offscreen data bits using offset +; ldx R4 ;reobtain position in buffer +; cmp #$00 +; bne ExYOfsBS ;if bits not zero, branch to leave +; dey ;otherwise, do bottom of the screen now +; bpl Loop +; ExYOfsBS: +; rts + +; YOffscreenBitsData: +; .byte $00, $08, $0c, $0e +; .byte $0f, $07, $03, $01 +; .byte $00 + +; DefaultYOnscreenOfs: +; .byte $04, $00, $04 + +; HighPosUnitData: +; .byte $ff, $00 + +; .endproc + +; GetOffScreenBitsSet: +; tya ;save offscreen bits offset to stack for now +; pha +; jsr GetXOffscreenBits ;do subroutine here +; lsr ;move high nybble to low +; lsr +; lsr +; lsr +; sta R0 ;store here +; jsr GetYOffscreenBits +; asl ;move low nybble to high nybble +; asl +; asl +; asl +; ora R0 ;mask together with previously saved low nybble +; sta R0 ;store both here +; pla ;get offscreen bits offset from stack +; tay +; lda R0 ;get value here and store elsewhere +; sta FBall_OffscreenBits +; ldx ObjectOffset +; rts + +; BoundingBoxCore: +; stx R0 ;save offset here + +; lda SprObject_Y_Position,x ;load vertical coordinate low +; sta R2 +; lda SprObject_X_Position,x ;load horizontal coordinate +; sec ;subtract left edge coordinate +; sbc ScreenLeft_X_Pos +; sta R1 + +; txa ;multiply offset by four and save to stack +; asl +; asl +; pha +; tay ;use as offset for Y, X is left alone + +; ;lda SprObj_BoundBoxCtrl,x ;load value here to be used as offset for X +; lda #0 +; asl ;multiply that by four and use as X +; asl +; tax +; lda R1 ;add the first number in the bounding box data to the +; clc ;relative horizontal coordinate using enemy object offset +; adc BoundBoxCtrlData,x ;and store somewhere using same offset * 4 +; sta BoundingBox_UL_Corner,y ;store here +; lda R1 +; clc +; adc BoundBoxCtrlData+2,x ;add the third number in the bounding box data to the +; sta BoundingBox_LR_Corner,y ;relative horizontal coordinate and store +; inx ;increment both offsets +; iny +; lda R2 ;add the second number to the relative vertical coordinate +; clc ;using incremented offset and store using the other +; adc BoundBoxCtrlData,x ;incremented offset +; sta BoundingBox_UL_Corner,y +; lda R2 +; clc +; adc BoundBoxCtrlData+2,x ;add the fourth number to the relative vertical coordinate +; sta BoundingBox_LR_Corner,y ;and store +; pla ;get original offset loaded into $00 * y from stack +; tay ;use as Y +; ldx R0 ;get original offset and use as X again +; rts + +; CheckRightScreenBBox: +; lda ScreenLeft_X_Pos ;add 128 pixels to left side of screen +; clc ;and store as horizontal coordinate of middle +; adc #$80 +; sta R2 +; lda ScreenLeft_PageLoc ;add carry to page location of left side of screen +; adc #$00 ;and store as page location of middle +; sta R1 +; lda SprObject_X_Position,x ;get horizontal coordinate +; cmp R2 ;compare against middle horizontal coordinate +; lda SprObject_PageLoc,x ;get page location +; sbc R1 ;subtract from middle page location +; bcc CheckLeftScreenBBox ;if object is on the left side of the screen, branch +; lda BoundingBox_DR_XPos,y ;check right-side edge of bounding box for offscreen +; bmi NoOfs ;coordinates, branch if still on the screen +; lda #$ff ;load offscreen value here to use on one or both horizontal sides +; ldx BoundingBox_UL_XPos,y ;check left-side edge of bounding box for offscreen +; bmi SORte ;coordinates, and branch if still on the screen +; sta BoundingBox_UL_XPos,y ;store offscreen value for left side +; SORte: +; sta BoundingBox_DR_XPos,y ;store offscreen value for right side +; NoOfs: +; ldx ObjectOffset ;get object offset and leave +; rts + +; CheckLeftScreenBBox: +; lda BoundingBox_UL_XPos,y ;check left-side edge of bounding box for offscreen +; bpl NoOfs2 ;coordinates, and branch if still on the screen +; cmp #$a0 ;check to see if left-side edge is in the middle of the +; bcc NoOfs2 ;screen or really offscreen, and branch if still on +; lda #$00 +; ldx BoundingBox_DR_XPos,y ;check right-side edge of bounding box for offscreen +; bpl SOLft ;coordinates, branch if still onscreen +; sta BoundingBox_DR_XPos,y ;store offscreen value for right side +; SOLft: +; sta BoundingBox_UL_XPos,y ;store offscreen value for left side +; NoOfs2: +; ldx ObjectOffset ;get object offset and leave +; rts + +BlockBufferChk_FBall: + ldy #$1a ;set offset for block buffer adder data + txa + clc + adc #FireballOffset ;add seven bytes to use + tax + lda #$00 ;set A to return vertical coordinate +BBChk_E: + jsr BlockBufferCollision ;do collision detection subroutine for sprite object + ldx ObjectOffset ;get object offset + cmp #$00 ;check to see if object bumped into anything + rts \ No newline at end of file diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index 21c419b31..3be547882 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -99,8 +99,735 @@ ; Reserve Metasprite 0 as the "unused" sprite. This sprite won't be drawn to the screen +.include "z2r.inc" + .segment "PRG0" +.export PlayerBankTable + +PRINT_METASPRITE_IDS = 0 + +METASPRITES_COUNT .set 0 + +PATTERN_TABLE_0 = $00 +PATTERN_TABLE_1 = $01 +SPRITE_BANK_0 = $00 | PATTERN_TABLE_0 +SPRITE_BANK_1 = $40 | PATTERN_TABLE_0 +SPRITE_BANK_2 = $80 | PATTERN_TABLE_0 +SPRITE_BANK_3 = $c0 | PATTERN_TABLE_0 +SPRITE_BANK_4 = $00 | PATTERN_TABLE_1 +SPRITE_BANK_5 = $40 | PATTERN_TABLE_1 +SPRITE_BANK_6 = $80 | PATTERN_TABLE_1 +SPRITE_BANK_7 = $c0 | PATTERN_TABLE_1 + + +MSPR_VERTICAL_FLIP = %10000000 +MSPR_OFFSET_MASK = %00111111 +.define MetaspriteOffset(b) 0+((b) & MSPR_OFFSET_MASK) + +; Custom values to signify that this sprite should be flipped in the metasprite +SPR_FLIP_H = %01000000 << 8 +SPR_FLIP_V = %10000000 << 8 +; Can be used to force a sprite palette. If NO_PALETTE is set, then the palette value +; from the object in the game will not be applied, and this value will be forced instead +SPR_NO_PALETTE = %00100000 << 8 +SPR_PALETTE_0 = SPR_NO_PALETTE | %00000000 << 8 +SPR_PALETTE_1 = SPR_NO_PALETTE | %00000001 << 8 +SPR_PALETTE_2 = SPR_NO_PALETTE | %00000010 << 8 +SPR_PALETTE_3 = SPR_NO_PALETTE | %00000011 << 8 + + +.define MetaspriteVramOffset(b, vram) (((b) & %00111111) | vram) + +.macro MetaspriteBoxBody Object, Animation, Direction, VramOffset, Palette, XOffset, YOffset, Spr1, Spr2, Spr3, Spr4 +.local X1, X2, Y1, Y2, Size +.local Tile1, Tile2, Tile3, Tile4 +.local Attr1, Attr2, Attr3, Attr4 +.local DrawSprite1, DrawSprite2, DrawSprite3, DrawSprite4 + +X1 = 0+XOffset +X2 = 8+XOffset +Y1 = 8+YOffset +Y2 = 24+YOffset +Size .set 1 + +DrawSprite2 .set 0 +DrawSprite3 .set 0 +DrawSprite4 .set 0 + +DrawSprite1 .set 1 +Tile1 .set MetaspriteVramOffset(Spr1) >> 8)) +.ifnblank Spr2 +Attr2 .set (Palette | ((>Spr2) >> 8)) +.endif +.ifnblank Spr3 +Attr3 .set (Palette | ((>Spr3) >> 8)) +.endif +.ifnblank Spr4 +Attr4 .set (Palette | ((>Spr4) >> 8)) +.endif + +.if .xmatch(Direction, "LEFT") +; .out .sprintf("Switching tile and attr %s %s %s", Object, Animation, Direction) +.ifnblank Spr2 + Tmp .set Tile1 + Tile1 .set Tile2 + Tile2 .set Tmp + Tmp .set Attr1 + Attr1 .set Attr2 + Attr2 .set Tmp + Attr1 .set Attr1 ^ $40 + Attr2 .set Attr2 ^ $40 +.endif +.if .blank(Spr4) .and(.not .blank(Spr3)) + DrawSprite4 .set 1 + DrawSprite3 .set 0 + Attr4 .set Attr3 ^ $40 + Tile4 .set Tile3 +.endif +.ifnblank Spr4 + Tmp .set Tile3 + Tile3 .set Tile4 + Tile4 .set Tmp + Tmp .set Attr3 + Attr3 .set Attr4 + Attr4 .set Tmp + Attr3 .set Attr3 ^ $40 + Attr4 .set Attr4 ^ $40 +.endif +.endif + +.ident( .sprintf("MetaspriteData_%s_%s_%s", Object, Animation, Direction) ): + .byte (Size * 4) +.if DrawSprite1 <> 0 + .byte Tile1, Attr1, Y1, X1 +.endif +.if DrawSprite2 <> 0 + .byte Tile2, Attr2, Y1, X2 +.endif +.if DrawSprite3 <> 0 + .byte Tile3, Attr3, Y2, X1 +.endif +.if DrawSprite4 <> 0 + .byte Tile4, Attr4, Y2, X2 +.endif +.endmacro + +.macro MetaspriteBox Object, Animation, Spr1, Spr2, Spr3, Spr4 +.local VramOffset, Bank, Palette, XOffset, YOffset, Mirror + +.if .defined(.ident( .sprintf("%s_%s_VRAM_OFFSET", Object, Animation) )) + VramOffset = .ident( .sprintf("%s_%s_VRAM_OFFSET", Object, Animation) ) +.elseif .defined(.ident( .sprintf("%s_VRAM_OFFSET", Object) )) + VramOffset = .ident( .sprintf("%s_VRAM_OFFSET", Object) ) +.else + VramOffset = 0 +.endif +.if .defined(.ident( .sprintf("%s_%s_BANK", Object, Animation) )) + Bank = .ident( .sprintf("%s_%s_BANK", Object, Animation) ) +.elseif .defined(.ident( .sprintf("%s_BANK", Object) )) + Bank = .ident( .sprintf("%s_BANK", Object) ) +.else + Bank = $ff +.endif +.if .defined(.ident( .sprintf("%s_%s_PALETTE", Object, Animation) )) + Palette = .ident( .sprintf("%s_%s_PALETTE", Object, Animation) ) +.elseif .defined(.ident( .sprintf("%s_PALETTE", Object) )) + Palette = .ident( .sprintf("%s_PALETTE", Object) ) +.else + .error .sprintf("Could not define Metasprite without the palette. Please define either %s_PALETTE or %s_%s_PALETTE", Object, Object, Animation) +.endif + +.if .defined(.ident( .sprintf("%s_%s_Y_OFFSET", Object, Animation) )) + YOffset = <.ident( .sprintf("%s_%s_Y_OFFSET", Object, Animation) ) +.elseif .defined(.ident( .sprintf("%s_Y_OFFSET", Object) )) + YOffset = <.ident( .sprintf("%s_Y_OFFSET", Object) ) +.else + YOffset = 0 +.endif + +.if .defined(.ident( .sprintf("%s_%s_X_OFFSET", Object, Animation) )) + XOffset = <.ident( .sprintf("%s_%s_X_OFFSET", Object, Animation) ) +.elseif .defined(.ident( .sprintf("%s_X_OFFSET", Object) )) + XOffset = <.ident( .sprintf("%s_X_OFFSET", Object) ) +.else + XOffset = 0 +.endif + +.if .defined(.ident( .sprintf("%s_%s_NO_MIRROR", Object, Animation) )) + Mirror = 0 +.elseif .defined(.ident( .sprintf("%s_NO_MIRROR", Object) )) + Mirror = 0 +.else + Mirror = 1 +.endif + +.ident( .sprintf("METASPRITE_%d_BANK", METASPRITES_COUNT) ) = Bank + +.ifdef METASPRITE_BODY + +.if Mirror = 1 +MetaspriteBoxBody Object, Animation, "LEFT", VramOffset, Palette, XOffset, YOffset, Spr1, Spr2, Spr3, Spr4 +.endif +MetaspriteBoxBody Object, Animation, "RIGHT", VramOffset, Palette, XOffset, YOffset, Spr1, Spr2, Spr3, Spr4 + +.if PRINT_METASPRITE_IDS +.out .sprintf("#define METASPRITE_%s_%s 0x%02x", Object, Animation, METASPRITES_COUNT) +.endif +.if Mirror = 1 +.ident( .sprintf("METASPRITE_LEFT_%d_LO", METASPRITES_COUNT) ) = .lobyte(.ident( .sprintf("MetaspriteData_%s_%s_LEFT", Object, Animation) )) +.ident( .sprintf("METASPRITE_LEFT_%d_HI", METASPRITES_COUNT) ) = .hibyte(.ident( .sprintf("MetaspriteData_%s_%s_LEFT", Object, Animation) )) +.else +.ident( .sprintf("METASPRITE_LEFT_%d_LO", METASPRITES_COUNT) ) = .lobyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) +.ident( .sprintf("METASPRITE_LEFT_%d_HI", METASPRITES_COUNT) ) = .hibyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) +.endif + +.ident( .sprintf("METASPRITE_RIGHT_%d_LO", METASPRITES_COUNT) ) = .lobyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) +.ident( .sprintf("METASPRITE_RIGHT_%d_HI", METASPRITES_COUNT) ) = .hibyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) + + +.endif + +MetaspriteReserve .sprintf("%s_%s",Object, Animation) +.endmacro + +.macro MetaspriteDuplicate Name, Mspr +.Local LL, LH, RL, RH, Id, Bank +Id = .ident( .concat("METASPRITE_", Mspr) ) +Bank = .ident( .sprintf("METASPRITE_%d_BANK", Id) ) +.ident( .sprintf("METASPRITE_%d_BANK", METASPRITES_COUNT) ) = Bank + +.ifdef METASPRITE_BODY +.if PRINT_METASPRITE_IDS +.out .sprintf("#define METASPRITE_%s 0x%02x // (duplicate of %s)", Name, METASPRITES_COUNT, Mspr) +.endif + +LL = .ident(.sprintf("METASPRITE_LEFT_%d_LO", Id)) +LH = .ident(.sprintf("METASPRITE_LEFT_%d_HI", Id)) +RL = .ident(.sprintf("METASPRITE_RIGHT_%d_LO", Id)) +RH = .ident(.sprintf("METASPRITE_RIGHT_%d_HI", Id)) + +.ident( .sprintf("METASPRITE_LEFT_%d_LO", METASPRITES_COUNT) ) = LL +.ident( .sprintf("METASPRITE_LEFT_%d_HI", METASPRITES_COUNT) ) = LH +.ident( .sprintf("METASPRITE_RIGHT_%d_LO", METASPRITES_COUNT) ) = RL +.ident( .sprintf("METASPRITE_RIGHT_%d_HI", METASPRITES_COUNT) ) = RH + +.endif + +MetaspriteReserve Name +.endmacro + + +;;;; +; Reserves a spot in the metasprite table for this named constant. +; This is used internally when making a new metasprite, but can also +; be used to make your own metasprite. Just reserve a slot with the name +; of the metasprite and +.macro MetaspriteReserve Name +.ident( .sprintf("METASPRITE_%s", Name) ) = METASPRITES_COUNT +.export .ident( .sprintf("METASPRITE_%s", Name) ) +METASPRITES_COUNT .set METASPRITES_COUNT + 1 +.endmacro + +.macro MetaspriteData Name, Left, Right +.Local Id + +Id = .ident(Name) + +.ident( .sprintf("METASPRITE_LEFT_%d_LO", Id) ) = .lobyte(Left) +.ident( .sprintf("METASPRITE_LEFT_%d_HI", Id) ) = .hibyte(Left) +.ifnblank Right +.ident( .sprintf("METASPRITE_RIGHT_%d_LO", Id) ) = .lobyte(Right) +.ident( .sprintf("METASPRITE_RIGHT_%d_HI", Id) ) = .hibyte(Right) +.else +.ident( .sprintf("METASPRITE_RIGHT_%d_LO", Id) ) = .lobyte(Left) +.ident( .sprintf("METASPRITE_RIGHT_%d_HI", Id) ) = .hibyte(Left) +.endif + +.endmacro + + +; .proc DrawAllMetasprites + +; LoopCount = M0 + +; lda #24 - 1 ; size of the different object update list +; sta LoopCount +; lda SpriteShuffleOffset +; clc +; adc #19 +; cmp #24 +; bcc :+ +; ; implicit carry set +; sbc #24 +; : +; sta SpriteShuffleOffset +; lda FrameCounter +; lsr +; lda SpriteShuffleOffset +; bcc ObjectLoopNegative + +; ObjectLoopPositive: +; clc +; adc #13 +; cmp #24 +; bcc :+ +; ; implicit carry set +; sbc #24 +; : +; ; skip index zero since we draw the player first always. +; beq NextLoop +; ; TODO check offscreenbits to make sure they are onscreen still +; tay +; ldx ObjectMetasprite,y +; beq NextLoop +; cpx #METASPRITES_COUNT ; todo remove this after fixing all bugs +; bcs NextLoop +; sta SpriteShuffleOffset +; jsr DrawMetasprite +; lda SpriteShuffleOffset +; NextLoop: +; dec LoopCount +; bpl ObjectLoopPositive +; jmp HandleFloateyNumbers + +; ObjectLoopNegative: +; sec +; sbc #13 +; bcs :+ +; ; implicit carry clear +; adc #24 +; : +; ; skip index zero since we draw the player first always. +; beq NextLoop2 +; ; TODO check offscreenbits to make sure they are onscreen still +; tay +; ldx ObjectMetasprite,y +; beq NextLoop2 +; cpx #METASPRITES_COUNT ; todo remove this after fixing all bugs +; bcs NextLoop2 +; sta SpriteShuffleOffset +; jsr DrawMetasprite +; lda SpriteShuffleOffset +; NextLoop2: +; dec LoopCount +; bpl ObjectLoopNegative + +; HandleFloateyNumbers: +; sta SpriteShuffleOffset + +; ldx #7-1 +; FloateyNumberLoop: +; lda FloateyNum_Control,x ;load control for floatey number +; beq Skip ;if zero, branch to leave +; phx +; jsr FloateyNumberRender +; plx +; Skip: +; dex +; bpl FloateyNumberLoop + + ; lda CurrentOAMOffset + ; pha + + ; ; put the player in slot 0 always + ; lda #0 + ; sta CurrentOAMOffset + + ; ; draw the player first so it doesn't ever flicker + ; ; first clear out the sprites that are reserved for the player + ; lda #$f8 + ; sta Sprite_Y_Position + 0 + ; sta Sprite_Y_Position + 4 + ; sta Sprite_Y_Position + 8 + ; sta Sprite_Y_Position + 12 + +; .if ::MOUSE_DISPLAY_CURSOR +; sta Sprite_Y_Position + 16 +; sta Sprite_Y_Position + 20 +; lda mouse +; bmi connected +; ; not connected so draw the alternate sprite at the middle of the screen +; lda #256 / 2 - 8 +; sta mouse + kMouseX +; lda #240 / 2 - 8 +; sta mouse + kMouseY + +; lda FrameCounter +; and #%00100000 +; beq useothersprite ; switch between connected and not connected +; ldy #1 +; bne loadmetasprite ;unconditional +; useothersprite: +; ldy #0 +; beq loadmetasprite +; connected: +; ldy MouseState +; loadmetasprite: +; ldx MouseStateMetasprite,y +; lda MetaspriteTableLeftLo,x +; sta R0 ; Ptr +; lda MetaspriteTableLeftHi,x +; sta R1 ; Ptr +; lda #0 +; sta R3 ; Atr +; lda mouse + kMouseX +; sta R4 ; Xlo +; lda #0 +; sta R5 ; Xhi +; lda mouse + kMouseY +; sta R6 ; Ylo +; lda #1 +; sta R7 ; Yhi +; jsr MetaspriteRenderLoop +; .endif + + ; ldy #0 + ; ldx ObjectMetasprite,y + ; ; unless the player is currently flickering due to damage taken + ; beq DoneDrawingPlayer + ; ; Load the bank for the player if it changed + ; lda PlayerBankTable,x + ; cmp PlayerChrBank + ; beq :+ + ; sta PlayerChrBank + ; inc ReloadCHRBank + ; : + ; jsr DrawMetasprite + ; DoneDrawingPlayer: + + ; Clear sprites up to the offset + ; Calculate CurrentOAMOffset / 4 * 3 and add that to the OAM clear to jump + ; the middle of the clear routine based on how many we need to clear + ; pla ; CurrentOAMOffset + ; lsr + ; sta M0 + ; lsr + ; ; clc - always cleared + ; adc M0 + ; ; clc - always cleared + ; adc #OAMClear + ; adc #0 + ; sta M1 + ; lda #$f8 + ; jmp (M0) + +; .if ::MOUSE_DISPLAY_CURSOR +; MouseStateMetasprite: +; .byte METASPRITE_MOUSE_POINTER +; .byte METASPRITE_MOUSE_DISCONNECTED +; .byte METASPRITE_MOUSE_PINCH +; .endif +; .endproc + +; MoveAllSpritesOffscreen: +; ldy #0 +; sty CurrentOAMOffset +; lda #$f8 +; OAMClear: +; .repeat 64, I +; sta Sprite_Y_Position + I*4 ; write 248 into OAM data's Y coordinate +; .endrepeat +; rts + + +.proc DrawMetasprite +Ptr = R0 +; OrigOffset = R2 +Atr = R3 +Xlo = R4 +Xhi = R5 +Ylo = R6 +Yhi = R7 +; LoopCount = M0 +; VFlip = M1 + + cpx #METASPRITES_COUNT + bcc + + rts ; Temp work around to prevent crashing when trying to render bad msprs + + + + lda PlayerFacingDir,y + lsr + bne FacingLeft + lda MetaspriteTableRightLo,x + sta Ptr + lda MetaspriteTableRightHi,x + sta Ptr+1 + bne DrawSprite ; unconditional + FacingLeft: + lda MetaspriteTableLeftLo,x + sta Ptr + lda MetaspriteTableLeftHi,x + sta Ptr+1 +DrawSprite: + + ; sty OrigOffset + + lda SprObject_X_Position,y + sec + sbc ScreenLeft_X_Pos + sta Xlo + lda SprObject_PageLoc,y + sbc ScreenLeft_PageLoc + sta Xhi + lda SprObject_Y_HighPos,y + sta Yhi + +; lda #0 +; sta VFlip +; cpy #7 +; bcs SkipVerticalFlipCheck +; lda ObjectVerticalFlip,y +; bpl NoVFlip +; ; Object has a vertical flip attribute set +; sta VFlip +; NoVFlip: +; ; Object has an added Y offset +; ; The negative flag is bit 5 in our number, so sign extend the value +; and #%00111111 +; cmp #%00100000 +; bcc PositiveYOffset +; ; subtracting a small negative offset here +; ora #%11000000 +; clc +; PositiveYOffset: +; adc SprObject_Y_Position,y +; bcc SetYOffset +; ; If the carry is set, then we need to inc Yhi ... TODO +; ; this probably doesn't handle underflow properly? +; inc Yhi +; bcs SetYOffset ; unconditional +; SkipVerticalFlipCheck: + lda SprObject_Y_Position,y +SetYOffset: + sta Ylo + +; lda VFlip +; beq DontSetFlipBit +; lda #OAM_FLIP_V +; .byte $2c ; masks the next lda #0 +; DontSetFlipBit: + lda #0 + ora SprObject_SprAttrib,y + sta Atr + + jsr MetaspriteRenderLoop + + +; lda VFlip +; beq DontShiftPositions +; ldy #0 +; lda (Ptr),y +; cmp #8 + 1 +; bcc DontShiftPositions +; cmp #12 +; beq Flip3 +; ; otherwise flip all 4 sprites +; lda Sprite_Tilenumber-4,x ;with first or second row tiles +; pha ;and save tiles to the stack +; lda Sprite_Tilenumber-8,x +; pha +; lda Sprite_Tilenumber-12,x ;exchange third row tiles +; sta Sprite_Tilenumber-4,x ;with first or second row tiles +; lda Sprite_Tilenumber-16,x +; sta Sprite_Tilenumber-8,x +; pla ;pull first or second row tiles from stack +; sta Sprite_Tilenumber-16,x ;and save in third row +; pla +; sta Sprite_Tilenumber-12,x +; rts +; Flip3: +; ; Custom flip code for bowser's front since he is weird. +; lda Sprite_Y_Position-4,x +; pha +; lda Sprite_Y_Position-12,x +; sta Sprite_Y_Position-8,x +; sta Sprite_Y_Position-4,x +; pla +; sta Sprite_Y_Position-12,x +; DontShiftPositions: + rts + +.endproc + +.proc MetaspriteRenderLoop +Ptr = R0 +Atr = R3 +Xlo = R4 +Xhi = R5 +Ylo = R6 +Yhi = R7 + + ldx CurrentOAMOffset + ldy #0 + lda (Ptr),y + tay + bpl RenderLoop + +; Offscreen sprites end up here + +Skip4: ; X Offscreen + dey +Skip3: ; Y Offscreen + dey +Skip2: + dey + ; Move this sprite offscreen + lda #$f8 + sta Sprite_Y_Position,x + inx + inx + inx + inx + dey + beq LoopEnded +RenderLoop: + ; load the x position and make sure its on screen + clc + lda (Ptr),y + bpl PositiveX + adc Xlo + sta Sprite_X_Position,x + lda Xhi + adc #$ff + beq ContinueAfterX + bne Skip4 + PositiveX: + adc Xlo + sta Sprite_X_Position,x + lda Xhi + adc #0 + bne Skip4 + ContinueAfterX: + dey + + ; load the y position and also make sure its on screen + clc + lda (Ptr),y + bpl PositiveY + ; NegativeY + adc Ylo + sta Sprite_Y_Position,x + lda Yhi + adc #$ff + cmp #1 ; page 1 is the "main" y position + beq ContinueAfterY + bne Skip3 + PositiveY: + adc Ylo + sta Sprite_Y_Position,x + lda Yhi + adc #0 + cmp #1 ; page 1 is the "main" y position + bne Skip3 + ContinueAfterY: + dey + + ; Mix attributes but if the NO_PALETTE bit is set, prevent + ; the palette from changing. + lda (Ptr),y + bit NoPaletteBitMask + beq AllowPaletteChange + ; No palette change bit set, so pull the byte and + ; mask off the palette from the attribute byte + lda Atr + and #%11111100 + ora (Ptr),y + bne WritePalette ; unconditional +AllowPaletteChange: + ora Atr +WritePalette: + sta Sprite_Attributes,x + dey + + ; set the tile number and move to the next sprite + lda (Ptr),y + sta Sprite_Tilenumber,x + inx + inx + inx + inx + dey + bne RenderLoop +LoopEnded: + stx CurrentOAMOffset + rts + +NoPaletteBitMask: + .byte (SPR_NO_PALETTE >> 8) +.endproc + +; ;------------------------------------------------------------------------------------- +; .proc FloateyNumberRender +; Ptr = R0 +; ; OrigOffset = R2 +; Atr = R3 +; Xlo = R4 +; Xhi = R5 +; Ylo = R6 +; Yhi = R7 + +; lda FloateyNum_Y_Pos,x ;get vertical coordinate for +; cmp #$18 ;floatey number, if coordinate in the +; bcc SetupNumSpr ;status bar, branch +; sbc #$01 +; sta FloateyNum_Y_Pos,x ;otherwise subtract one and store as new +; SetupNumSpr: +; sta Ylo +; lda #1 +; sta Yhi +; lda #0 +; sta Atr +; sta Xhi +; lda FloateyNum_X_Pos,x +; sta Xlo +; ldy FloateyNum_Control,x +; ldx FloateyNumMetasprites-1,y +; lda MetaspriteTableRightLo,x +; sta Ptr +; lda MetaspriteTableRightHi,x +; sta Ptr+1 +; jmp MetaspriteRenderLoop +; ; implicit rts + +; FloateyNumMetasprites: +; .byte METASPRITE_NUMBER_100 +; .byte METASPRITE_NUMBER_200 +; .byte METASPRITE_NUMBER_400 +; .byte METASPRITE_NUMBER_500 +; .byte METASPRITE_NUMBER_800 +; .byte METASPRITE_NUMBER_1000 +; .byte METASPRITE_NUMBER_2000 +; .byte METASPRITE_NUMBER_4000 +; .byte METASPRITE_NUMBER_5000 +; .byte METASPRITE_NUMBER_8000 +; .byte METASPRITE_NUMBER_1UP + +; .endproc + +.export DrawMetasprite + METASPRITE_BODY = 1 MetaspriteReserve "NULL" @@ -131,6 +858,7 @@ MetaspriteBox "BIG_MARIO", "SWIMMING_1_HOLD", $00, $02, $24, $22 MetaspriteBox "BIG_MARIO", "SWIMMING_2_HOLD", $04, $06, $24, $26 MetaspriteBox "BIG_MARIO", "SWIMMING_3_HOLD", $04, $06, $28, $22 +.export FIRE_MARIO_OFFSET FIRE_MARIO_OFFSET = METASPRITES_COUNT - METASPRITE_BIG_MARIO_STANDING FIRE_MARIO_VRAM_OFFSET = SPRITE_BANK_0 FIRE_MARIO_BANK = CHR_MARIOACTION @@ -190,6 +918,7 @@ MetaspriteBox "SMALL_FIRE", "WALKING_3", $00, $02, $0c, $0e MetaspriteBox "SMALL_FIRE", "SKIDDING", $00, $02, $10, $12 MetaspriteBox "SMALL_FIRE", "JUMPING", $00, $02, $14, $16 +.export SWIMMING_ANIMATION_FRAME_COUNT SWIMMING_ANIMATION_FRAME_COUNT = METASPRITE_BIG_MARIO_SWIMMING_1_HOLD - METASPRITE_BIG_MARIO_SWIMMING_1_KICK MetaspriteBox "SMALL_FIRE", "SWIMMING_1_KICK", $00, $02, $34, $36 MetaspriteBox "SMALL_FIRE", "SWIMMING_2_KICK", $00, $02, $30, $32 diff --git a/RandomizerCore/Asm/z2mario/metasprite_engine.s b/RandomizerCore/Asm/z2mario/metasprite_engine.s deleted file mode 100644 index cf135a0fe..000000000 --- a/RandomizerCore/Asm/z2mario/metasprite_engine.s +++ /dev/null @@ -1,722 +0,0 @@ - -.segment "PRG0" - -PRINT_METASPRITE_IDS = 0 - -METASPRITES_COUNT .set 0 - -PATTERN_TABLE_0 = $00 -PATTERN_TABLE_1 = $01 -SPRITE_BANK_0 = $00 | PATTERN_TABLE_0 -SPRITE_BANK_1 = $40 | PATTERN_TABLE_0 -SPRITE_BANK_2 = $80 | PATTERN_TABLE_0 -SPRITE_BANK_3 = $c0 | PATTERN_TABLE_0 -SPRITE_BANK_4 = $00 | PATTERN_TABLE_1 -SPRITE_BANK_5 = $40 | PATTERN_TABLE_1 -SPRITE_BANK_6 = $80 | PATTERN_TABLE_1 -SPRITE_BANK_7 = $c0 | PATTERN_TABLE_1 - - -MSPR_VERTICAL_FLIP = %10000000 -MSPR_OFFSET_MASK = %00111111 -.define MetaspriteOffset(b) 0+((b) & MSPR_OFFSET_MASK) - -; Custom values to signify that this sprite should be flipped in the metasprite -SPR_FLIP_H = %01000000 << 8 -SPR_FLIP_V = %10000000 << 8 -; Can be used to force a sprite palette. If NO_PALETTE is set, then the palette value -; from the object in the game will not be applied, and this value will be forced instead -SPR_NO_PALETTE = %00100000 << 8 -SPR_PALETTE_0 = SPR_NO_PALETTE | %00000000 << 8 -SPR_PALETTE_1 = SPR_NO_PALETTE | %00000001 << 8 -SPR_PALETTE_2 = SPR_NO_PALETTE | %00000010 << 8 -SPR_PALETTE_3 = SPR_NO_PALETTE | %00000011 << 8 - - -.define MetaspriteVramOffset(b, vram) (((b) & %00111111) | vram) - -.macro MetaspriteBoxBody Object, Animation, Direction, VramOffset, Palette, XOffset, YOffset, Spr1, Spr2, Spr3, Spr4 -.local X1, X2, Y1, Y2, Size -.local Tile1, Tile2, Tile3, Tile4 -.local Attr1, Attr2, Attr3, Attr4 -.local DrawSprite1, DrawSprite2, DrawSprite3, DrawSprite4 - -X1 = 0+XOffset -X2 = 8+XOffset -Y1 = 8+YOffset -Y2 = 24+YOffset -Size .set 1 - -DrawSprite2 .set 0 -DrawSprite3 .set 0 -DrawSprite4 .set 0 - -DrawSprite1 .set 1 -Tile1 .set MetaspriteVramOffset(Spr1) >> 8)) -.ifnblank Spr2 -Attr2 .set (Palette | ((>Spr2) >> 8)) -.endif -.ifnblank Spr3 -Attr3 .set (Palette | ((>Spr3) >> 8)) -.endif -.ifnblank Spr4 -Attr4 .set (Palette | ((>Spr4) >> 8)) -.endif - -.if .xmatch(Direction, "LEFT") -; .out .sprintf("Switching tile and attr %s %s %s", Object, Animation, Direction) -.ifnblank Spr2 - Tmp .set Tile1 - Tile1 .set Tile2 - Tile2 .set Tmp - Tmp .set Attr1 - Attr1 .set Attr2 - Attr2 .set Tmp - Attr1 .set Attr1 ^ $40 - Attr2 .set Attr2 ^ $40 -.endif -.if .blank(Spr4) .and(.not .blank(Spr3)) - DrawSprite4 .set 1 - DrawSprite3 .set 0 - Attr4 .set Attr3 ^ $40 - Tile4 .set Tile3 -.endif -.ifnblank Spr4 - Tmp .set Tile3 - Tile3 .set Tile4 - Tile4 .set Tmp - Tmp .set Attr3 - Attr3 .set Attr4 - Attr4 .set Tmp - Attr3 .set Attr3 ^ $40 - Attr4 .set Attr4 ^ $40 -.endif -.endif - -.ident( .sprintf("MetaspriteData_%s_%s_%s", Object, Animation, Direction) ): - .byte (Size * 4) -.if DrawSprite1 <> 0 - .byte Tile1, Attr1, Y1, X1 -.endif -.if DrawSprite2 <> 0 - .byte Tile2, Attr2, Y1, X2 -.endif -.if DrawSprite3 <> 0 - .byte Tile3, Attr3, Y2, X1 -.endif -.if DrawSprite4 <> 0 - .byte Tile4, Attr4, Y2, X2 -.endif -.endmacro - -.macro MetaspriteBox Object, Animation, Spr1, Spr2, Spr3, Spr4 -.local VramOffset, Bank, Palette, XOffset, YOffset, Mirror - -.if .defined(.ident( .sprintf("%s_%s_VRAM_OFFSET", Object, Animation) )) - VramOffset = .ident( .sprintf("%s_%s_VRAM_OFFSET", Object, Animation) ) -.elseif .defined(.ident( .sprintf("%s_VRAM_OFFSET", Object) )) - VramOffset = .ident( .sprintf("%s_VRAM_OFFSET", Object) ) -.else - VramOffset = 0 -.endif -.if .defined(.ident( .sprintf("%s_%s_BANK", Object, Animation) )) - Bank = .ident( .sprintf("%s_%s_BANK", Object, Animation) ) -.elseif .defined(.ident( .sprintf("%s_BANK", Object) )) - Bank = .ident( .sprintf("%s_BANK", Object) ) -.else - Bank = $ff -.endif -.if .defined(.ident( .sprintf("%s_%s_PALETTE", Object, Animation) )) - Palette = .ident( .sprintf("%s_%s_PALETTE", Object, Animation) ) -.elseif .defined(.ident( .sprintf("%s_PALETTE", Object) )) - Palette = .ident( .sprintf("%s_PALETTE", Object) ) -.else - .error .sprintf("Could not define Metasprite without the palette. Please define either %s_PALETTE or %s_%s_PALETTE", Object, Object, Animation) -.endif - -.if .defined(.ident( .sprintf("%s_%s_Y_OFFSET", Object, Animation) )) - YOffset = <.ident( .sprintf("%s_%s_Y_OFFSET", Object, Animation) ) -.elseif .defined(.ident( .sprintf("%s_Y_OFFSET", Object) )) - YOffset = <.ident( .sprintf("%s_Y_OFFSET", Object) ) -.else - YOffset = 0 -.endif - -.if .defined(.ident( .sprintf("%s_%s_X_OFFSET", Object, Animation) )) - XOffset = <.ident( .sprintf("%s_%s_X_OFFSET", Object, Animation) ) -.elseif .defined(.ident( .sprintf("%s_X_OFFSET", Object) )) - XOffset = <.ident( .sprintf("%s_X_OFFSET", Object) ) -.else - XOffset = 0 -.endif - -.if .defined(.ident( .sprintf("%s_%s_NO_MIRROR", Object, Animation) )) - Mirror = 0 -.elseif .defined(.ident( .sprintf("%s_NO_MIRROR", Object) )) - Mirror = 0 -.else - Mirror = 1 -.endif - -.ident( .sprintf("METASPRITE_%d_BANK", METASPRITES_COUNT) ) = Bank - -.ifdef METASPRITE_BODY - -.if Mirror = 1 -MetaspriteBoxBody Object, Animation, "LEFT", VramOffset, Palette, XOffset, YOffset, Spr1, Spr2, Spr3, Spr4 -.endif -MetaspriteBoxBody Object, Animation, "RIGHT", VramOffset, Palette, XOffset, YOffset, Spr1, Spr2, Spr3, Spr4 - -.if PRINT_METASPRITE_IDS -.out .sprintf("#define METASPRITE_%s_%s 0x%02x", Object, Animation, METASPRITES_COUNT) -.endif -.if Mirror = 1 -.ident( .sprintf("METASPRITE_LEFT_%d_LO", METASPRITES_COUNT) ) = .lobyte(.ident( .sprintf("MetaspriteData_%s_%s_LEFT", Object, Animation) )) -.ident( .sprintf("METASPRITE_LEFT_%d_HI", METASPRITES_COUNT) ) = .hibyte(.ident( .sprintf("MetaspriteData_%s_%s_LEFT", Object, Animation) )) -.else -.ident( .sprintf("METASPRITE_LEFT_%d_LO", METASPRITES_COUNT) ) = .lobyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) -.ident( .sprintf("METASPRITE_LEFT_%d_HI", METASPRITES_COUNT) ) = .hibyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) -.endif - -.ident( .sprintf("METASPRITE_RIGHT_%d_LO", METASPRITES_COUNT) ) = .lobyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) -.ident( .sprintf("METASPRITE_RIGHT_%d_HI", METASPRITES_COUNT) ) = .hibyte(.ident( .sprintf("MetaspriteData_%s_%s_RIGHT", Object, Animation) )) - - -.endif - -MetaspriteReserve .sprintf("%s_%s",Object, Animation) -.endmacro - -.macro MetaspriteDuplicate Name, Mspr -.Local LL, LH, RL, RH, Id, Bank -Id = .ident( .concat("METASPRITE_", Mspr) ) -Bank = .ident( .sprintf("METASPRITE_%d_BANK", Id) ) -.ident( .sprintf("METASPRITE_%d_BANK", METASPRITES_COUNT) ) = Bank - -.ifdef METASPRITE_BODY -.if PRINT_METASPRITE_IDS -.out .sprintf("#define METASPRITE_%s 0x%02x // (duplicate of %s)", Name, METASPRITES_COUNT, Mspr) -.endif - -LL = .ident(.sprintf("METASPRITE_LEFT_%d_LO", Id)) -LH = .ident(.sprintf("METASPRITE_LEFT_%d_HI", Id)) -RL = .ident(.sprintf("METASPRITE_RIGHT_%d_LO", Id)) -RH = .ident(.sprintf("METASPRITE_RIGHT_%d_HI", Id)) - -.ident( .sprintf("METASPRITE_LEFT_%d_LO", METASPRITES_COUNT) ) = LL -.ident( .sprintf("METASPRITE_LEFT_%d_HI", METASPRITES_COUNT) ) = LH -.ident( .sprintf("METASPRITE_RIGHT_%d_LO", METASPRITES_COUNT) ) = RL -.ident( .sprintf("METASPRITE_RIGHT_%d_HI", METASPRITES_COUNT) ) = RH - -.endif - -MetaspriteReserve Name -.endmacro - - -;;;; -; Reserves a spot in the metasprite table for this named constant. -; This is used internally when making a new metasprite, but can also -; be used to make your own metasprite. Just reserve a slot with the name -; of the metasprite and -.macro MetaspriteReserve Name -.ident( .sprintf("METASPRITE_%s", Name) ) = METASPRITES_COUNT -METASPRITES_COUNT .set METASPRITES_COUNT + 1 -.endmacro - -.macro MetaspriteData Name, Left, Right -.Local Id - -Id = .ident(Name) - -.ident( .sprintf("METASPRITE_LEFT_%d_LO", Id) ) = .lobyte(Left) -.ident( .sprintf("METASPRITE_LEFT_%d_HI", Id) ) = .hibyte(Left) -.ifnblank Right -.ident( .sprintf("METASPRITE_RIGHT_%d_LO", Id) ) = .lobyte(Right) -.ident( .sprintf("METASPRITE_RIGHT_%d_HI", Id) ) = .hibyte(Right) -.else -.ident( .sprintf("METASPRITE_RIGHT_%d_LO", Id) ) = .lobyte(Left) -.ident( .sprintf("METASPRITE_RIGHT_%d_HI", Id) ) = .hibyte(Left) -.endif - -.endmacro - - -; .proc DrawAllMetasprites - -; LoopCount = M0 - -; lda #24 - 1 ; size of the different object update list -; sta LoopCount -; lda SpriteShuffleOffset -; clc -; adc #19 -; cmp #24 -; bcc :+ -; ; implicit carry set -; sbc #24 -; : -; sta SpriteShuffleOffset -; lda FrameCounter -; lsr -; lda SpriteShuffleOffset -; bcc ObjectLoopNegative - -; ObjectLoopPositive: -; clc -; adc #13 -; cmp #24 -; bcc :+ -; ; implicit carry set -; sbc #24 -; : -; ; skip index zero since we draw the player first always. -; beq NextLoop -; ; TODO check offscreenbits to make sure they are onscreen still -; tay -; ldx ObjectMetasprite,y -; beq NextLoop -; cpx #METASPRITES_COUNT ; todo remove this after fixing all bugs -; bcs NextLoop -; sta SpriteShuffleOffset -; jsr DrawMetasprite -; lda SpriteShuffleOffset -; NextLoop: -; dec LoopCount -; bpl ObjectLoopPositive -; jmp HandleFloateyNumbers - -; ObjectLoopNegative: -; sec -; sbc #13 -; bcs :+ -; ; implicit carry clear -; adc #24 -; : -; ; skip index zero since we draw the player first always. -; beq NextLoop2 -; ; TODO check offscreenbits to make sure they are onscreen still -; tay -; ldx ObjectMetasprite,y -; beq NextLoop2 -; cpx #METASPRITES_COUNT ; todo remove this after fixing all bugs -; bcs NextLoop2 -; sta SpriteShuffleOffset -; jsr DrawMetasprite -; lda SpriteShuffleOffset -; NextLoop2: -; dec LoopCount -; bpl ObjectLoopNegative - -; HandleFloateyNumbers: -; sta SpriteShuffleOffset - -; ldx #7-1 -; FloateyNumberLoop: -; lda FloateyNum_Control,x ;load control for floatey number -; beq Skip ;if zero, branch to leave -; phx -; jsr FloateyNumberRender -; plx -; Skip: -; dex -; bpl FloateyNumberLoop - - ; lda CurrentOAMOffset - ; pha - - ; ; put the player in slot 0 always - ; lda #0 - ; sta CurrentOAMOffset - - ; ; draw the player first so it doesn't ever flicker - ; ; first clear out the sprites that are reserved for the player - ; lda #$f8 - ; sta Sprite_Y_Position + 0 - ; sta Sprite_Y_Position + 4 - ; sta Sprite_Y_Position + 8 - ; sta Sprite_Y_Position + 12 - -; .if ::MOUSE_DISPLAY_CURSOR -; sta Sprite_Y_Position + 16 -; sta Sprite_Y_Position + 20 -; lda mouse -; bmi connected -; ; not connected so draw the alternate sprite at the middle of the screen -; lda #256 / 2 - 8 -; sta mouse + kMouseX -; lda #240 / 2 - 8 -; sta mouse + kMouseY - -; lda FrameCounter -; and #%00100000 -; beq useothersprite ; switch between connected and not connected -; ldy #1 -; bne loadmetasprite ;unconditional -; useothersprite: -; ldy #0 -; beq loadmetasprite -; connected: -; ldy MouseState -; loadmetasprite: -; ldx MouseStateMetasprite,y -; lda MetaspriteTableLeftLo,x -; sta R0 ; Ptr -; lda MetaspriteTableLeftHi,x -; sta R1 ; Ptr -; lda #0 -; sta R3 ; Atr -; lda mouse + kMouseX -; sta R4 ; Xlo -; lda #0 -; sta R5 ; Xhi -; lda mouse + kMouseY -; sta R6 ; Ylo -; lda #1 -; sta R7 ; Yhi -; jsr MetaspriteRenderLoop -; .endif - - ; ldy #0 - ; ldx ObjectMetasprite,y - ; ; unless the player is currently flickering due to damage taken - ; beq DoneDrawingPlayer - ; ; Load the bank for the player if it changed - ; lda PlayerBankTable,x - ; cmp PlayerChrBank - ; beq :+ - ; sta PlayerChrBank - ; inc ReloadCHRBank - ; : - ; jsr DrawMetasprite - ; DoneDrawingPlayer: - - ; Clear sprites up to the offset - ; Calculate CurrentOAMOffset / 4 * 3 and add that to the OAM clear to jump - ; the middle of the clear routine based on how many we need to clear - ; pla ; CurrentOAMOffset - ; lsr - ; sta M0 - ; lsr - ; ; clc - always cleared - ; adc M0 - ; ; clc - always cleared - ; adc #OAMClear - ; adc #0 - ; sta M1 - ; lda #$f8 - ; jmp (M0) - -; .if ::MOUSE_DISPLAY_CURSOR -; MouseStateMetasprite: -; .byte METASPRITE_MOUSE_POINTER -; .byte METASPRITE_MOUSE_DISCONNECTED -; .byte METASPRITE_MOUSE_PINCH -; .endif -; .endproc - -; MoveAllSpritesOffscreen: -; ldy #0 -; sty CurrentOAMOffset -; lda #$f8 -; OAMClear: -; .repeat 64, I -; sta Sprite_Y_Position + I*4 ; write 248 into OAM data's Y coordinate -; .endrepeat -; rts - - -.proc DrawMetasprite -Ptr = R0 -; OrigOffset = R2 -Atr = R3 -Xlo = R4 -Xhi = R5 -Ylo = R6 -Yhi = R7 -; LoopCount = M0 -; VFlip = M1 - - cpx #METASPRITES_COUNT - bcc + - rts ; Temp work around to prevent crashing when trying to render bad msprs - + - - lda PlayerFacingDir,y - lsr - bne FacingLeft - lda MetaspriteTableRightLo,x - sta Ptr - lda MetaspriteTableRightHi,x - sta Ptr+1 - bne DrawSprite ; unconditional - FacingLeft: - lda MetaspriteTableLeftLo,x - sta Ptr - lda MetaspriteTableLeftHi,x - sta Ptr+1 -DrawSprite: - - ; sty OrigOffset - - lda SprObject_X_Position,y - sec - sbc ScreenLeft_X_Pos - sta Xlo - lda SprObject_PageLoc,y - sbc ScreenLeft_PageLoc - sta Xhi - lda SprObject_Y_HighPos,y - sta Yhi - -; lda #0 -; sta VFlip -; cpy #7 -; bcs SkipVerticalFlipCheck -; lda ObjectVerticalFlip,y -; bpl NoVFlip -; ; Object has a vertical flip attribute set -; sta VFlip -; NoVFlip: -; ; Object has an added Y offset -; ; The negative flag is bit 5 in our number, so sign extend the value -; and #%00111111 -; cmp #%00100000 -; bcc PositiveYOffset -; ; subtracting a small negative offset here -; ora #%11000000 -; clc -; PositiveYOffset: -; adc SprObject_Y_Position,y -; bcc SetYOffset -; ; If the carry is set, then we need to inc Yhi ... TODO -; ; this probably doesn't handle underflow properly? -; inc Yhi -; bcs SetYOffset ; unconditional -; SkipVerticalFlipCheck: - lda SprObject_Y_Position,y -SetYOffset: - sta Ylo - -; lda VFlip -; beq DontSetFlipBit -; lda #OAM_FLIP_V -; .byte $2c ; masks the next lda #0 -; DontSetFlipBit: - lda #0 - ora SprObject_SprAttrib,y - sta Atr - - jsr MetaspriteRenderLoop - - -; lda VFlip -; beq DontShiftPositions -; ldy #0 -; lda (Ptr),y -; cmp #8 + 1 -; bcc DontShiftPositions -; cmp #12 -; beq Flip3 -; ; otherwise flip all 4 sprites -; lda Sprite_Tilenumber-4,x ;with first or second row tiles -; pha ;and save tiles to the stack -; lda Sprite_Tilenumber-8,x -; pha -; lda Sprite_Tilenumber-12,x ;exchange third row tiles -; sta Sprite_Tilenumber-4,x ;with first or second row tiles -; lda Sprite_Tilenumber-16,x -; sta Sprite_Tilenumber-8,x -; pla ;pull first or second row tiles from stack -; sta Sprite_Tilenumber-16,x ;and save in third row -; pla -; sta Sprite_Tilenumber-12,x -; rts -; Flip3: -; ; Custom flip code for bowser's front since he is weird. -; lda Sprite_Y_Position-4,x -; pha -; lda Sprite_Y_Position-12,x -; sta Sprite_Y_Position-8,x -; sta Sprite_Y_Position-4,x -; pla -; sta Sprite_Y_Position-12,x -; DontShiftPositions: - rts - -.endproc - -.proc MetaspriteRenderLoop -Ptr = R0 -Atr = R3 -Xlo = R4 -Xhi = R5 -Ylo = R6 -Yhi = R7 - - ldx CurrentOAMOffset - ldy #0 - lda (Ptr),y - tay - bpl RenderLoop - -; Offscreen sprites end up here - -Skip4: ; X Offscreen - dey -Skip3: ; Y Offscreen - dey -Skip2: - dey - ; Move this sprite offscreen - lda #$f8 - sta Sprite_Y_Position,x - inx - inx - inx - inx - dey - beq LoopEnded -RenderLoop: - ; load the x position and make sure its on screen - clc - lda (Ptr),y - bpl PositiveX - adc Xlo - sta Sprite_X_Position,x - lda Xhi - adc #$ff - beq ContinueAfterX - bne Skip4 - PositiveX: - adc Xlo - sta Sprite_X_Position,x - lda Xhi - adc #0 - bne Skip4 - ContinueAfterX: - dey - - ; load the y position and also make sure its on screen - clc - lda (Ptr),y - bpl PositiveY - ; NegativeY - adc Ylo - sta Sprite_Y_Position,x - lda Yhi - adc #$ff - cmp #1 ; page 1 is the "main" y position - beq ContinueAfterY - bne Skip3 - PositiveY: - adc Ylo - sta Sprite_Y_Position,x - lda Yhi - adc #0 - cmp #1 ; page 1 is the "main" y position - bne Skip3 - ContinueAfterY: - dey - - ; Mix attributes but if the NO_PALETTE bit is set, prevent - ; the palette from changing. - lda (Ptr),y - bit NoPaletteBitMask - beq AllowPaletteChange - ; No palette change bit set, so pull the byte and - ; mask off the palette from the attribute byte - lda Atr - and #%11111100 - ora (Ptr),y - bne WritePalette ; unconditional -AllowPaletteChange: - ora Atr -WritePalette: - sta Sprite_Attributes,x - dey - - ; set the tile number and move to the next sprite - lda (Ptr),y - sta Sprite_Tilenumber,x - inx - inx - inx - inx - dey - bne RenderLoop -LoopEnded: - stx CurrentOAMOffset - rts - -NoPaletteBitMask: - .byte (SPR_NO_PALETTE >> 8) -.endproc - -; ;------------------------------------------------------------------------------------- -; .proc FloateyNumberRender -; Ptr = R0 -; ; OrigOffset = R2 -; Atr = R3 -; Xlo = R4 -; Xhi = R5 -; Ylo = R6 -; Yhi = R7 - -; lda FloateyNum_Y_Pos,x ;get vertical coordinate for -; cmp #$18 ;floatey number, if coordinate in the -; bcc SetupNumSpr ;status bar, branch -; sbc #$01 -; sta FloateyNum_Y_Pos,x ;otherwise subtract one and store as new -; SetupNumSpr: -; sta Ylo -; lda #1 -; sta Yhi -; lda #0 -; sta Atr -; sta Xhi -; lda FloateyNum_X_Pos,x -; sta Xlo -; ldy FloateyNum_Control,x -; ldx FloateyNumMetasprites-1,y -; lda MetaspriteTableRightLo,x -; sta Ptr -; lda MetaspriteTableRightHi,x -; sta Ptr+1 -; jmp MetaspriteRenderLoop -; ; implicit rts - -; FloateyNumMetasprites: -; .byte METASPRITE_NUMBER_100 -; .byte METASPRITE_NUMBER_200 -; .byte METASPRITE_NUMBER_400 -; .byte METASPRITE_NUMBER_500 -; .byte METASPRITE_NUMBER_800 -; .byte METASPRITE_NUMBER_1000 -; .byte METASPRITE_NUMBER_2000 -; .byte METASPRITE_NUMBER_4000 -; .byte METASPRITE_NUMBER_5000 -; .byte METASPRITE_NUMBER_8000 -; .byte METASPRITE_NUMBER_1UP - -; .endproc diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s index 2e9a639b2..a83c3906f 100644 --- a/RandomizerCore/Asm/z2mario/sfx.s +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -1,4 +1,8 @@ +.include "z2r.inc" + +.export Square1SfxHandler, Square2SfxHandler, NoiseSfxHandler + .segment "PRG6" ;------------------------------------------------------------------------------------- diff --git a/RandomizerCore/Asm/z2mario/sprites_mario.chr b/RandomizerCore/Asm/z2mario/sprites_mario.chr new file mode 100644 index 0000000000000000000000000000000000000000..7c8180cd222c9a76539af1935c6ea1dae6a7b37b GIT binary patch literal 4096 zcmbtWF^n5U6dkiB>*O4-Nl?xZ?SFX`+)AtSu5~8YzW?E_ z5Cz+DDhCKJgySAO<<*MV6MMOKX?|V^CDXhUVX^9oEEm;5TZmK|gS}+uy0AtB2u%c* zBd-SgAP~CZP^%@z1jLv?69j~miU_1p(;LGl`P-ZPMI(&2;4h@gGJ=2gPV2N4#xLyM zdwfZF-PoA*hwVn_d!9J^z#%6bJCjl`dP!B(sv^sB(`zL8U=YV9*S9YJVT>3%Fys#h z78j#l&4~?p;EI6}#b4m-l@BY}&9#gvEaV^T0eSL2zq|DA2simVeV&smqi1%WvmZRw z8#uk;z}NR5s=t)#M&Y2}4+5=e6Pj~lepE_j-`#Oqi1QvjXt#qP3`grB4f4!Aee8%; zz)J(iRGJ82)OgNl+{S|GiW1Jf*Xs=i)hH1(4|l*GtyasiEo6{m+ej#@U=@lqtE-|^ zuIsEy`^i*hN~+aF(%hvPkuU^^2O3dTT{kpFce{S+=zW!8{x|52_9&b2!x4jUDbe4EpZ?-3~7#mh>XO4&@4gm996FwC7xFz;gD zTxYA1AOj{(lj?8xr9|?9(+#qJ+^cn9-@)qe>~Q7|;ZJ&gKSm5-d1YmNechP*H!r;w zz!}}|`GKZ#X&?y1;_>5$4~+Td=Dh>cJ=ugOv^1D_B1F_q7X`oAmt2Tceqt zmrh(6ez$bu<1-gm1_8Z$``q$y-nM@C9^9&aJ&8zw5P2-O#Xmv)qsE4nHb0LDU{K9b z9&HY!UUmcdvv##s>foi?faP|%DCS(ZftlwY?PqU->3iA$V>AB%r=msGjsh@apVlYh z$L(Wvc$Q;0@tx)$PUqhic$esH<{v#z4z6dDYxKJ8MfZF9VgR2S_( ziLQzSjXV*SZSR}IMzj_B1)#3mUO*E)T&sEu1tE2_Ssh9pt}hTQfT@psrZYfNX}jTp zgC+N*sM3|ga!#H>L@x{i*A^A$jed}-j__(JJ|_QHedsx4UEnUD1QS{T=a+>yZDp)9!EOKkyy0A4L&<$^5<->y5dRsM3|$aq40CDsHaAI(6nO zsbDJ4zv=hZs-lDjo+;&P{UxPddi&EQobF8ikGctZ;5hs<`Huu3g_ESF&k%#%)&I8r z`w99#*FRqWT>p6eb38<7s{T2i*FVSe`k%qG`sa9F{~XWjpW}J`&)`}8b3E#As(v`0 z*B{68`y texts, ROM rom, StatRandomizer randomizedStats) + private void MarioModeActivate(Assembler asm) + { + // "metasprite_engine.s" is included in metasprite.s + string[] modules = ["integration.s", "map.s", "mario.s", "metasprite.s", "sfx.s"]; + foreach (var mod in modules) + { + var a = asm.Module(); + a.Assign("ENABLE_Z2_MARIO", 1); + a.Code(Util.ReadResource($"Z2Randomizer.RandomizerCore.Asm.z2mario.{mod}"), mod); + } + + } + + private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Random RNG, List texts, ROM rom, StatRandomizer randomizedStats) { bool randomizeMusic = !props.DisableMusic && props.RandomizeMusic; @@ -3810,6 +3831,11 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando StatTracking(props, engine); AddCredits(engine); + if (props.MarioMode) + { + MarioModeActivate(engine); + } + if (props.ShuffleBossHP != EnemyLifeOption.VANILLA) { rom.SetBossHpBarDivisors(engine, randomizedStats); diff --git a/RandomizerCore/RandomizerCore.csproj b/RandomizerCore/RandomizerCore.csproj index afe3c142d..9e58b9743 100644 --- a/RandomizerCore/RandomizerCore.csproj +++ b/RandomizerCore/RandomizerCore.csproj @@ -71,5 +71,19 @@ + + + + + + + + + + + + + + From 0e707df1e310d4212ff568bb0549f364f7a1955b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 27 Jan 2026 03:00:15 -0500 Subject: [PATCH 006/128] Fix lives screen mario, and prevent shooting fire without fire active --- RandomizerCore/Asm/z2mario/integration.s | 31 +++++++++++++++++++++--- RandomizerCore/Asm/z2mario/mario.s | 7 ++++-- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 04f70a730..4b90c9131 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -47,13 +47,38 @@ BankSwitchMarioCHR: jmp PatchLinkLivesScreenDraw .reloc PatchLinkLivesScreenDraw: +.import METASPRITE_BIG_MARIO_STANDING + lda #METASPRITE_BIG_MARIO_STANDING + sta ObjectMetasprite + lda #CHR_BIGMARIO + sta PlayerChrBank + inc ReloadCHRBank + lda #$78 + sta SprObject_X_Position + lda #$50 + sta SprObject_Y_Position lda #1 - sta Player_Y_HighPos - ; also force the bank to be correct - jsr PlayerGfxHandler + sta SprObject_Y_HighPos + sta PlayerFacingDir + lsr ; a = 0 + sta SprObject_SprAttrib + sta ScreenLeft_X_Pos + lda ScreenLeft_PageLoc + sta SprObject_PageLoc jsr BankSwitchMarioCHR jmp $EC02 +; patch death screen to bank the metasprites properly +.org $c9f1 + jsr PatchLinkDeathSprite + nop + +.reloc +PatchLinkDeathSprite: + jsr PlayerGfxHandler + jmp BankSwitchMarioCHR + + ; patch the link draw routine to skip drawing him with the vanilla code .org $EC02 ; ldx $11 lda $29,x jsr PatchLinkDrawRoutine diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 47d714a2e..ed979d71b 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -2334,13 +2334,16 @@ PlayerHeadCollision: lda #$00 sta Player_Y_Speed ;init player's vertical speed - +ExitFireballNearby: rts ;leave! ProcFireball_Bubble: ; lda PlayerStatus ;check player's status ; cmp #$02 -; bcc ProcAirBubbles ;if not fiery, branch +; bcc >rts ;if not fiery, branch + lda $076F + and #$10 + beq ExitFireballNearby lda A_B_Buttons and #B_Button ;check for b button pressed beq ProcFireballs ;branch if not pressed From 14be7720f65b622425a156f191cc1f5eaf0123ca Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 31 Jan 2026 11:46:05 -0500 Subject: [PATCH 007/128] First public teaser video made --- CommandLine/DefaultPlayerOptions.json | 6 +- CommandLine/Properties/launchSettings.json | 3 +- RandomizerCore/Asm/Init.s | 2 + RandomizerCore/Asm/MMC5.s | 3 + RandomizerCore/Asm/z2mario/hammer_suit.s | 0 RandomizerCore/Asm/z2mario/integration.s | 176 +++++++++++++++--- RandomizerCore/Asm/z2mario/mario.s | 73 ++++++-- ...tes_mario.chr => sprites_mario_hammer.chr} | Bin 4096 -> 4096 bytes RandomizerCore/Asm/z2r.inc | 128 +++++++------ RandomizerCore/Hyrule.cs | 2 +- RandomizerCore/Music.cs | 4 +- RandomizerCore/ROM.cs | 2 +- RandomizerCore/RandomizerCore.csproj | 4 +- 13 files changed, 289 insertions(+), 114 deletions(-) create mode 100644 RandomizerCore/Asm/z2mario/hammer_suit.s rename RandomizerCore/Asm/z2mario/{sprites_mario.chr => sprites_mario_hammer.chr} (86%) diff --git a/CommandLine/DefaultPlayerOptions.json b/CommandLine/DefaultPlayerOptions.json index f52d70332..f9ab2bbf6 100644 --- a/CommandLine/DefaultPlayerOptions.json +++ b/CommandLine/DefaultPlayerOptions.json @@ -1,7 +1,7 @@ { "DisableMusic": false, - "RandomizeMusic": false, - "MixCustomAndOriginalMusic": true, + "RandomizeMusic": true, + "MixCustomAndOriginalMusic": false, "IncludeDiverseMusic": false, "DisableUnsafeMusic": true, "FastSpellCasting": true, @@ -16,5 +16,5 @@ "ShieldTunicColor": "Default", "BeamSprite": "Default", "BeepThreshold": "Normal", - "BeepFrequency": "Normal" + "BeepFrequency": "Off" } \ No newline at end of file diff --git a/CommandLine/Properties/launchSettings.json b/CommandLine/Properties/launchSettings.json index f6598c862..cb373ceee 100644 --- a/CommandLine/Properties/launchSettings.json +++ b/CommandLine/Properties/launchSettings.json @@ -2,7 +2,8 @@ "profiles": { "CommandLine": { "commandName": "Project", - "commandLineArgs": "-f AAAA2dJAJBAAAAAAABALyPyk+hAAAAAAAAAAAAABAC -r ../../../../Zelda2.nes -s 12345678 -o ../../../.." + "commandLineArgs": "-f AAAA2dJAJBAAAAAAABALyPyk+hAAAAAAAAAAAAABAC -r ../Zelda2.nes -s 32165874 -o ..", + "workingDirectory": "../build/" } } } \ No newline at end of file diff --git a/RandomizerCore/Asm/Init.s b/RandomizerCore/Asm/Init.s index 88747cacb..360031d45 100644 --- a/RandomizerCore/Asm/Init.s +++ b/RandomizerCore/Asm/Init.s @@ -32,6 +32,8 @@ ; Mark unused areas in the ROM so the linker can place stuff here ; FREE "PRG0" [$AA40, $c000) +;FREE "PRG0" [$A89E, $A980) +FREE "PRG0" [$A89E, $A970) ; Reserve some space for z2ft FREE "PRG0" [$AB00, $c000) ; give room for z2edit to patch $aa40 FREE "PRG1" [$87c6, $88a0) diff --git a/RandomizerCore/Asm/MMC5.s b/RandomizerCore/Asm/MMC5.s index ee634661f..7ee140bae 100644 --- a/RandomizerCore/Asm/MMC5.s +++ b/RandomizerCore/Asm/MMC5.s @@ -521,7 +521,10 @@ SwapCHR: ; 1kb sprite banks are 20-27 and the 1kb bg banks are 28-2b asl asl +.ifndef ENABLE_Z2_MARIO + ; do NOT change the first sprite bank since thats controlling the mario graphics sta SpChrBank0Reg +.endif adc #1 sta SpChrBank1Reg adc #1 diff --git a/RandomizerCore/Asm/z2mario/hammer_suit.s b/RandomizerCore/Asm/z2mario/hammer_suit.s new file mode 100644 index 000000000..e69de29bb diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 4b90c9131..2f7967764 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -14,14 +14,14 @@ FREE "PRG0" [$92BF, $962D) ; .org (($10ea - $10) .mod $4000) + $8000 ; .byte $17 - .segment "PRG0", "PRG7" +SpellCastingRoutine = $8DC3 ; Link Main .org $D3EC ; patches the main sideview routine right before checking marios code jsr SwapMarioCHRBanks .reloc SwapMarioCHRBanks: jsr BankSwitchMarioCHR - jmp $8DC3 + jmp SpellCastingRoutine .reloc BankSwitchMarioCHR: @@ -37,6 +37,8 @@ BankSwitchMarioCHR: sta SpChrBank0Reg + 0 lda CurrentCHRBank ; we need to write a BG bank register due to MMC5 jank sta BgChrBank0Reg + lda #0 + sta ReloadCHRBank + rts @@ -68,6 +70,18 @@ PatchLinkLivesScreenDraw: jsr BankSwitchMarioCHR jmp $EC02 +; Jump to our own metasprite setting routine +.org $919e + jsr SetHurtMetasprite ; Just jumped into lava bro +.reloc +SetHurtMetasprite: +.import METASPRITE_SMALL_MARIO_DEATH + lda #METASPRITE_SMALL_MARIO_DEATH + sta ObjectMetasprite + lda #$0b ; player death subroutine + sta GameEngineSubroutine + rts + ; patch death screen to bank the metasprites properly .org $c9f1 jsr PatchLinkDeathSprite @@ -75,7 +89,12 @@ PatchLinkLivesScreenDraw: .reloc PatchLinkDeathSprite: + lda #METASPRITE_SMALL_MARIO_DEATH + sta ObjectMetasprite + lda #$0b ; player death subroutine + sta GameEngineSubroutine jsr PlayerGfxHandler + inc ReloadCHRBank jmp BankSwitchMarioCHR @@ -91,27 +110,24 @@ PatchLinkDrawRoutine: lda $29,x rts + - ; for now assume its always PRG0 (it seems to be at least in my limited testing) - ; jsr SwapToPRG0 ; put the player in slot 4 always lda #4 sta CurrentOAMOffset - ; draw the player first so it doesn't ever flicker - ; first clear out the sprites that are reserved for the player - ; lda #$f8 - ; sta Sprite_Y_Position + 4 - ; sta Sprite_Y_Position + 8 - ; sta Sprite_Y_Position + 12 - ; sta Sprite_Y_Position + 16 - ; sta Sprite_Y_Position + FireballOffset * 4 - ; sta Sprite_Y_Position + (FireballOffset+1) * 4 - ldx ObjectMetasprite beq @skipplayer ldy #0 jsr DrawMetasprite + ; Update the player bank if the metasprite has changed +.import PlayerBankTable + ldy ObjectMetasprite + lda PlayerBankTable,y + cmp PlayerChrBank + beq + + sta PlayerChrBank + inc ReloadCHRBank + + @skipplayer: ; While we are here, lets draw the fireball/hammer sprites too @@ -126,13 +142,65 @@ PatchLinkDrawRoutine: ldy R2 cpy #ProjectileOffset+2 ; Draw two fireballs and one hammer bne @fireballLoop - - ; jsr SwapToSavedPRG - ; double return + ; double return to skip the OG drawing code pla pla rts +.org $88AF + jmp OverworldLateInit +.reloc +OverworldLateInit: + sta $0736 ; Sets game mode + jmp UpdatePlayerPalette + +.org $8D61 + ; patches over an unused write for $0727 in sideview init + jsr UpdatePlayerPalette +.segment "PRG7" +.reloc +UpdatePlayerPalette: + inc ReloadCHRBank ; Just reload the CHR bank to be safe here + ; Check fire state and update the palette if we are fire still + ldx #0 + lda #7 + sta R7 + lda $76f + and #$10 + beq + + ldx #7 + + + ; we are still firey so write a palette update for next NMI + ldy $301 + - + lda PlayerRegularPalettePPUCommand,x + sta $302,y + iny + inx + dec R7 + bne - + tya + clc + adc #7 + sta $301 + rts +PlayerRegularPalettePPUCommand: + .byte $3f, $11, $03, $16, $27, $18, $ff +PlayerFirePalettePPUCommand: + .byte $3f, $11, $03, $37, $27, $16, $ff + +.segment "PRG0", "PRG7" +; Don't clear fire state when transitioning to side views +.org $8cf4 + jsr DontClearMagicState +.reloc +; prevent fire from going away in screen transitions but clear everything else +DontClearMagicState: + lda $76f + and #$10 + sta $76f + tya + rts ; patch end of link's loading routine .org $90DB ; sta $69DE @@ -153,13 +221,54 @@ SetupMarioControl: .org $9041 .word (GameRoutines) -; .reloc -; NewLinkMarioProcess: - ; TODO: Process all the other stuff to decide what parts of mario update to run - ; jsr GameRoutines - ; rts -; .org $9041 -; .word (GameRoutines) + +.segment "PRG7" +; Has to be in fixed bank +.org $e337 + jsr LinkTookDamage +.reloc +LinkTookDamage: + sta $774 + php ; Keep carry since it uses that to detect if link has died later + jsr CheckIfTurningSmall + plp + rts +.reloc +CheckIfTurningSmall: + ; Check player size, if HP is less than 2 bars we are small mario + lda $0774 + cmp #65 + bcs + + lda GameEngineSubroutine + cmp #8 ; if player state is already something else don't try to change + bne + + lda PlayerSize ; if player is already small don't make small + bne + + ; If we are fire mario, we lost the fire power, so reset our palette + lda $76f + and #~$10 + sta $76f + jsr UpdatePlayerPalette + + ; Set player routine to ChangeSize + lda #$0a + sta GameEngineSubroutine + lda #8 + sta InjuryTimer + lda #1 + ; sta PlayerChangeSizeFlag + sta ScrollLock + sta Player_State + lda #0 + sta ScrollAmount + ; sta PlayerSize + lda #$ff + sta TimerControl ;set master timer control flag to halt timers + lda #Sfx_PipeDown_Injury + sta Square1SoundQueue ;load bump sound + + + rts +.segment "PRG0", "PRG7" .org $D4F3 ; patch the call to links main routine to set jsr PatchLinkMain @@ -343,7 +452,9 @@ UpdateCollisionFromMap: .reloc SlightlyModifiedCollisionRoutine: jsr SwapToSavedPRG - jsr $E095 ; skip ahead to near the end of the collision check + ldx #0 ; Make sure that the player data is getting used for collision +; jsr $E095 ; skip ahead to near the end of the collision check + jsr $E079 ; skip ahead to near the end of the collision check jmp SwapToPRG0 ; .org $E073 @@ -525,6 +636,21 @@ ElevatorMakeMarioStateStanding: ; bcc $ED1B ; nop +; Disable vanilla recoil when jumping off enemies +.org $e747 +; jsr DisableRecoilIfDownstab + jmp *+3 ; skip setting recoil for stabs + +SetLinkRecoil = $e371 + +.reloc +;DisableRecoilIfDownstab: +; jsr CheckForDownstab +; bcs + +; jmp SetLinkRecoil +; + +; rts + ; Make mario bounce off armored enemies .org $E65E jsr CheckForDownstab diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index ed979d71b..2a3237900 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -26,14 +26,21 @@ sta $078b ;hammer sta $0788 ;boots sta $0787 ;raft + sta $077b ; shield spell + sta $077c ; jump spell + sta $077d ; life spell + sta $077e ; fairy spell sta $077f ; fire spell + sta $0780 ; reflect spell + sta $0781 ; spell spell + sta $0782 ; thunder spell lda #8 sta $0783 ; magic jar count sta $0778 ; magic level - lda #$ff - sta $0773 ;infinite magic - sta $0774 ;infinite health +; lda #$ff +; sta $0773 ;infinite magic +; sta $0774 ;infinite health ;.endif lda GameEngineSubroutine ;run routine based on number (a few of these routines are @@ -175,6 +182,9 @@ SaveJoyp: sta Left_Right_Buttons ;if pressing down while on the ground, sta Up_Down_Buttons ;nullify directional bits SizeChk: + lda $a7 ; flip collision bits to match mario style + eor #$ff + sta $a7 jsr PlayerMovementSubs ;run movement subroutines ldy #$01 ;is player small? lda PlayerSize @@ -196,10 +206,6 @@ PlayerSubs: ; jsr $9610 ; update mario scroll position - ; lda $a7 ; flip collision bits to match mario style - ; eor #$ff - ; sta $a7 - jsr ScrollHandler ;move the screen if necessary ; jsr GetPlayerOffscreenBits ;get player's offscreen bits ; jsr RelativePlayerPosition ;get coordinates relative to the screen @@ -207,12 +213,15 @@ PlayerSubs: ; jsr BoundingBoxCore ;get player's bounding box coordinates jsr PlayerBGCollision ;do collision detection and process + lda $a7 ; flip collision bits to match z2 style + eor #$ff + sta $a7 + ; This is the offset for the table that holds the Y add val for link ldy #$1d ; Jump to the middle of the original link collision routine ; jsr $e070 ; link original bg collision routine jsr SlightlyModifiedCollisionRoutine - rts ; lda Player_Y_Position @@ -541,13 +550,6 @@ CntPl: ; Go to the next frame of the glitch animation inc ObjectMetasprite Exit: - ldy ObjectMetasprite - lda PlayerBankTable,y - cmp PlayerChrBank - beq + - sta PlayerChrBank - inc ReloadCHRBank - + rts ;then leave ClearMarioSprite: @@ -685,10 +687,18 @@ PlayerChangeSize: EndChgSize: cmp #$c4 ;check again for another specific moment bne ExitChgSize ;and branch to leave if before or after that point + ; Z2Mario - clear scroll lock after you change size + lda #0 + sta ScrollLock jmp DonePlayerTask ;otherwise do sub to init timer control and set routine ExitChgSize: rts ; TODO check this RTS can be removed ;and then leave +FinishedInjuryBlink: + ; Z2Mario - clear scroll lock after you change size + lda #0 + sta ScrollLock + beq DonePlayerTask ;------------------------------------------------------------------------------------- ; .reloc PlayerInjuryBlink: @@ -696,7 +706,7 @@ PlayerInjuryBlink: cmp #$f0 ;for specific moment in time bcs ExitBlink ;branch if before that point cmp #$c8 ;check again for another specific point - beq DonePlayerTask ;branch if at that point, and not before or after + beq FinishedInjuryBlink ;branch if at that point, and not before or after jmp PlayerCtrlRoutine ;otherwise run player control routine ExitBlink: bne ExitBoth ;do unconditional branch to leave @@ -1817,9 +1827,11 @@ RetYC: lda R3 ;get saved content of block buffer rts ;and leave FailedToGetBlock: + ; If we are leaving the map, then treat the offscreen blocks as no collision pla lda #0 sta R3 + sta R4 rts ; GetBlockBufferAddr: @@ -2000,7 +2012,14 @@ HeadChk: beq DoFootCheck ;player, and branch if nothing above player's head ; jsr CheckForCoinMTiles ;check to see if player touched coin with their head ; bcs AwardTouchedCoin ;if so, branch to some other part of code - ldy Player_Y_Speed ;check player's vertical speed + ; Z2 - also check if we are on an elevator + pha + lda $754 ; Player on elevator flag == $10 if on elevator + eor #$ff ; so invert it to make us have "upwards" speed if on ele + ora Player_Y_Speed ;check player's vertical speed + tay + pla + cpy #0 bpl DoFootCheck ;if player not moving upwards, branch elsewhere ldy R4 ;check lower nybble of vertical coordinate returned cpy #$04 ;from collision detection routine @@ -2018,8 +2037,15 @@ HeadChk: SolidOrClimb: ; cmp #$26 ;if climbing metatile, ; beq NYSpd ;branch ahead and do not play sound - lda #Sfx_Bump - sta Square1SoundQueue ;otherwise load bump sound + ; Z2 Set Player_CollisionBits to fix elevator +; lda #~$08 +; and Player_CollisionBits +; sta Player_CollisionBits + ; Z2 - don't set bump sound if on an elevator + lda $754 + bne NYSpd + lda #Sfx_Bump + sta Square1SoundQueue ;otherwise load bump sound NYSpd: lda #$01 ;set player's vertical speed to nullify sta Player_Y_Speed ;jump or swim @@ -2074,6 +2100,12 @@ LandPlyr: lda #$00 sta Player_Y_Speed ;initialize vertical speed and fractional sta Player_Y_MoveForce ;movement force to stop player's vertical movement + + ; Z2 Set Player_CollisionBits to fix elevator +; lda #~$04 +; and Player_CollisionBits +; sta Player_CollisionBits + ; sta StompChainCounter ;initialize enemy stomp counter ; cancel downstab hitbox?? lda #$f8 @@ -2331,7 +2363,6 @@ PlayerHeadCollision: ; eor #$01 ;and floatey numbers ; sta SprDataOffset_Ctrl - lda #$00 sta Player_Y_Speed ;init player's vertical speed ExitFireballNearby: @@ -2402,6 +2433,8 @@ FireballObjCore: lda #$01 ;set high byte of vertical position sta Fireball_Y_HighPos,x ldy PlayerFacingDir ;get player's facing direction + tya ; Z2 needs a value set for the knockback value in $6d + sta $6d,x dey ;decrement to use as offset here lda FireballXSpdData,y ;set horizontal speed of fireball accordingly sta Fireball_X_Speed,x diff --git a/RandomizerCore/Asm/z2mario/sprites_mario.chr b/RandomizerCore/Asm/z2mario/sprites_mario_hammer.chr similarity index 86% rename from RandomizerCore/Asm/z2mario/sprites_mario.chr rename to RandomizerCore/Asm/z2mario/sprites_mario_hammer.chr index 7c8180cd222c9a76539af1935c6ea1dae6a7b37b..3c6ff14d401efab905f9ff745546ec606c41875f 100644 GIT binary patch delta 409 zcmZorXi(U2lu?|GkB6pQ^W*yzKn6^VhR`5(j9$MzgzmPiF0Zozu~WU% tY&QR4K1r^9tYq28PPToVWZB1kgmDsIz~l>T4x0n`Y8XvO4IxlS0RUNDLU;fG diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 7b330b998..30138889b 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -145,51 +145,6 @@ NoiseSoundQueue = $f0 Square2SoundQueue = $f1 Square1SoundQueue = $f2 -;; Collectable items -; Hardcoded list of item ids for the new global item table. -; These should match the constants from the Collectables Enum -ITEM_CANDLE = $00 -ITEM_GLOVE = $01 -ITEM_RAFT = $02 -ITEM_BOOTS = $03 -ITEM_FLUTE = $04 -ITEM_CROSS = $05 -ITEM_HAMMER = $06 -ITEM_MAGIC_KEY = $07 - -ITEM_KEY = $08 -ITEM_DO_NOT_USE = $09 - -ITEM_SMALL_PBAG = $0a -ITEM_MEDIUM_PBAG = $0b -ITEM_LARGE_PBAG = $0c -ITEM_XL_PBAG = $0d -ITEM_MAGIC_CONTAINER = $0e -ITEM_HEART_CONTAINER = $0f -ITEM_BLUE_MAGIC_JAR = $10 -ITEM_RED_MAGIC_JAR = $11 -ITEM_1UP = $12 -ITEM_CHILD = $13 -ITEM_TROPHY = $14 -ITEM_MEDICINE = $15 -ITEM_DO_NOT_USE_ANTIFAIRY = $16 - -ITEM_UPSTAB = $17 -ITEM_DOWNSTAB = $18 -ITEM_BAGU = $19 -ITEM_MIRROR = $1a -ITEM_WATER = $1b - -ITEM_SHIELD_SPELL = $1c -ITEM_JUMP_SPELL = $1d -ITEM_LIFE_SPELL = $1e -ITEM_FAIRY_SPELL = $1f -ITEM_FIRE_SPELL = $20 -ITEM_REFLECT_SPELL = $21 -ITEM_SPELL_SPELL = $22 -ITEM_THUNDER_SPELL = $23 -ITEM_DASH_SPELL = $24 - ;; NEW VARIABLE ALLOCATIONS ; Stack RAM @@ -246,7 +201,7 @@ RESV DeathMusicLoaded RESV EventMusicQueue RESV EventMusicBuffer ; TODO use the vanilla music queues RESV WarpZoneControl -; RESV Player_CollisionBits +RESV Player_CollisionBits ; Split Z2 and SMB1 collision bits RESV HalfwayPage RESV SwimmingFlag RESV CurrentOAMOffset @@ -323,7 +278,7 @@ SprObject_Y_Position = Player_Y_Position Player_Y_HighPos = $19 SprObject_Y_HighPos = Player_Y_HighPos -Player_CollisionBits = $A7 +;Player_CollisionBits = $A7 ScrollAmount = $14 ObjectMetasprite = $80 @@ -383,8 +338,8 @@ Sprite_Data = Sprite_Y_Position CHR_BIGMARIO = $6a CHR_MARIOACTION = $6b -CHR_SMALLFIRE = $6c -CHR_SMALLMARIO = $6d +CHR_SMALLMARIO = $6c +CHR_SMALLFIRE = $6d A_Button = %10000000 B_Button = %01000000 @@ -445,16 +400,18 @@ StatTrackingSaveFileClear = $7401 .export NmiBankShadow8, NmiBankShadowA ; Reserve these in SRAM to prevent issues when clearing ram -NmiBankShadow8 = $6380 -NmiBankShadowA = $6381 -SoftDisableNmi = $6382 +NmiBankShadow8 = $6340 +NmiBankShadowA = $6341 +SoftDisableNmi = $6342 ; Flag to store if the dialog has ended DialogCompleteTmp = $0523 -COLLISION_TILES = $6390 +COLLISION_TILES = $6350 -StatTrackingBase = COLLISION_TILES + $d0 * 4 +;StatTrackingBase = COLLISION_TILES + $d0 * 4 +; Move stat tracking to after the save ram since Z2FT (custom music) uses this block +StatTrackingBase = $773C SET_RES_BASE StatTrackingBase ;; The follow stats are NOT checkpointed because they persist between reloads @@ -519,18 +476,21 @@ RESV TimestampTypeList, TS_COUNT RESV TimestampList, TS_COUNT * 3 CHECKPOINT_LEN = RES_OFFSET - CHECKPOINT_START +; NOTE: We can overlap this with the pause menu drawing code. +; Reserve 256 bytes to store a sideview buffer. +; This stores the command data for drawing a sideview, so its only needed temporarily +; when loading a side view, which is why its safe to alias this with the pause menu drawing. +SideViewBuffer = $7800 + ; Duplicate the timestamp memory here that we'll copy after saving RESV Checkpoint, CHECKPOINT_LEN STAT_TRACKING_SIZE = RES_OFFSET -; Reserve 256 bytes to store a sideview buffer. -; The sideview loading code is patched to quickly bank switch to the extended banks for palace data, -; or load regular sideviews from the usual location -RESV SideViewBuffer, $0100 - ClearSRAMStart = RES_OFFSET +.assert RES_OFFSET + StatTrackingBase < $7800 .ifdef ENABLE_Z2_MARIO +SET_RES_BASE $6FB0 ; There isn't a general purpose "attribute" mix so we can make our own RESV SprObject_SprAttrib, 16 Player_SprAttrib = SprObject_SprAttrib @@ -540,7 +500,6 @@ RESV Player_YMoveForceFractional, 16 ClearSRAMEnd = RES_OFFSET -.assert RES_OFFSET + StatTrackingBase < $6957 ;;; Update a handful of refs to point to the given address. ;;; Usage: @@ -583,4 +542,53 @@ UPDATE_BYTE target @ refs .endif ; ENABLE_Z2_MARIO + +;; Collectable items +; Hardcoded list of item ids for the new global item table. +; These should match the constants from the Collectables Enum +ITEM_CANDLE = $00 +ITEM_GLOVE = $01 +ITEM_RAFT = $02 +ITEM_BOOTS = $03 +ITEM_FLUTE = $04 +ITEM_CROSS = $05 +ITEM_HAMMER = $06 +ITEM_MAGIC_KEY = $07 + +ITEM_KEY = $08 +ITEM_DO_NOT_USE = $09 + +ITEM_SMALL_PBAG = $0a +ITEM_MEDIUM_PBAG = $0b +ITEM_LARGE_PBAG = $0c +ITEM_XL_PBAG = $0d +ITEM_MAGIC_CONTAINER = $0e +ITEM_HEART_CONTAINER = $0f +ITEM_BLUE_MAGIC_JAR = $10 +ITEM_RED_MAGIC_JAR = $11 +ITEM_1UP = $12 +ITEM_CHILD = $13 +ITEM_TROPHY = $14 +ITEM_MEDICINE = $15 +ITEM_DO_NOT_USE_ANTIFAIRY = $16 + +ITEM_UPSTAB = $17 +ITEM_DOWNSTAB = $18 +ITEM_BAGU = $19 +ITEM_MIRROR = $1a +ITEM_WATER = $1b + +ITEM_SHIELD_SPELL = $1c +ITEM_JUMP_SPELL = $1d +ITEM_LIFE_SPELL = $1e +ITEM_FAIRY_SPELL = $1f +ITEM_FIRE_SPELL = $20 +ITEM_REFLECT_SPELL = $21 +ITEM_SPELL_SPELL = $22 +ITEM_THUNDER_SPELL = $23 +ITEM_DASH_SPELL = $24 + + + + .endif diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 306f502e8..fc87247b4 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2422,7 +2422,7 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra rom.Put(ROM.ChrRomOffset + 0x01000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.randomizer_text.chr")); if (props.MarioMode) { - rom.Put(ROM.ChrRomOffset + 0x1a800, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.sprites_mario.chr")); + rom.Put(ROM.ChrRomOffset + 0x1a800, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.sprites_mario_hammer.chr")); var span = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.map_mario.chr"); rom.Put(ROM.ChrRomOffset + 0x11a00, span[0..0x220]); } diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index 7dd572ba1..e43315c28 100644 --- a/RandomizerCore/Music.cs +++ b/RandomizerCore/Music.cs @@ -237,7 +237,9 @@ public void ImportSongs() void WriteLogLine(string? line) { - Trace.WriteLine(line); +#if DEBUG + Console.WriteLine(line); +#endif _spoilerSb.AppendLine(line); } diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 6ef10d972..ca6023603 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -924,7 +924,7 @@ jmp LoadNewLevelCap nop LoadNewLevelCapReturn: ; $9F7F -.org $A89E +.reloc LoadNewLevelCap: lda $0777,X ; the instruction we overwrote with jmp cmp LevelCaps,X diff --git a/RandomizerCore/RandomizerCore.csproj b/RandomizerCore/RandomizerCore.csproj index 9e58b9743..10da275ca 100644 --- a/RandomizerCore/RandomizerCore.csproj +++ b/RandomizerCore/RandomizerCore.csproj @@ -83,7 +83,7 @@ - - + + From a4b6bae93edd9df4b3b8b1aa1adbb16c7e60a40b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 31 Jan 2026 15:38:42 -0500 Subject: [PATCH 008/128] Make mario big when gaining life --- RandomizerCore/Asm/z2mario/integration.s | 51 ++++++++++++++++++++++-- RandomizerCore/Asm/z2mario/mario.s | 5 +++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 2f7967764..ac5667b4b 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -255,10 +255,11 @@ CheckIfTurningSmall: sta GameEngineSubroutine lda #8 sta InjuryTimer + ; we will unlock the scroll lock after the size change animation finishes lda #1 - ; sta PlayerChangeSizeFlag sta ScrollLock sta Player_State + ; sta PlayerChangeSizeFlag lda #0 sta ScrollAmount ; sta PlayerSize @@ -268,8 +269,45 @@ CheckIfTurningSmall: sta Square1SoundQueue ;load bump sound + rts + .segment "PRG0", "PRG7" +; Make mario grow when his HP increases enough +.org $d42f ; patches over the sound effect for HP gain + jsr LinkGainedHPOrMagic + nop +.reloc +LinkGainedHPOrMagic: + lda #$10 + sta $ef ; Set the sfx for gaining life/magic/exp + ; if we are small and gaining HP + cpx #1 ; IF x = 1 we are increasing HP + bne @exit + lda $0774 + cmp #65 + bcc @exit + lda GameEngineSubroutine + cmp #8 ; if player state is already something else don't try to change + bne @exit + lda PlayerSize ; if player is already large don't make large + beq @exit + ; Set player routine to ChangeSize + lda #$0a + sta GameEngineSubroutine + ; we will unlock the scroll lock after the size change animation finishes + lda #1 + sta ScrollLock + lda #0 + sta ScrollAmount + sta Player_State + lda #$ff + sta TimerControl ;set master timer control flag to halt timers + ; Don't try to play the sound here since we might be locked out of this in a room right now +; lda #Sfx_PowerUpGrab +; sta Square2SoundQueue ;load grow up sound +@exit: + rts + .org $D4F3 ; patch the call to links main routine to set jsr PatchLinkMain .reloc @@ -493,10 +531,15 @@ SlightlyModifiedCollisionRoutine: UPDATE_BYTE $27 @ $285b $40b0 $40c0 $40d0 $40e0 $80b0 $80c0 $80d0 $80e0 $c0b0 $c0c0 $c0d0 $c0e0 $c0f0 $100b0 $100c0 $100d0 $100e0 $140b0 $140c0 $140d0 $140e0 $17c1a $1c465 $1c47d UPDATE_BYTE $18 @ $285C $40b1 $40c1 $40d1 $80e1 $80b1 $80c1 $80d1 $80e1 $c0b1 $c0c1 $c0d1 $c0e1 $100b1 $100c1 $100d1 $100e1 $140b1 $140c1 $140d1 $140e1 $17c1b $1c466 $1c47e ;.endif -; Fire spell color -UPDATE_BYTE $37 @ $2a0a +; Fire spell color. EDIT: This is regrettably not the fire spell, but a generic "after spell flash color" +; so it applies to anything that sets a spell color like grabbing a fairy or casting any spell or learning a spell, etc. +;UPDATE_BYTE $37 @ $2a0a +;UPDATE_BYTE $27 @ $2a10 +;UPDATE_BYTE $16 @ $2a16 +; So we just use mario's colors and decide later if we need to change to fire colors. +UPDATE_BYTE $16 @ $2a0a UPDATE_BYTE $27 @ $2a10 -UPDATE_BYTE $16 @ $2a16 +UPDATE_BYTE $18 @ $2a16 ; regular palette color location diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 2a3237900..e89520ada 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -716,6 +716,11 @@ InitChangeSize: bne ExitBoth ;then branch to leave sty PlayerAnimCtrl ;otherwise initialize player's animation frame control inc PlayerChangeSizeFlag ;set growing/shrinking flag + lda Player_State + bne + + lda #Sfx_PowerUpGrab + sta Square2SoundQueue ;load grow up sound + + lda PlayerSize eor #$01 ;invert player's size sta PlayerSize From 6888a9bfe1375124223fee80f69a8e22bf63456b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 2 Feb 2026 01:32:14 -0500 Subject: [PATCH 009/128] Add mostly working hammer --- RandomizerCore/Asm/z2mario/hammer_suit.s | 0 RandomizerCore/Asm/z2mario/integration.s | 8 +- RandomizerCore/Asm/z2mario/mario.s | 233 +++++++++++++++++++++++ RandomizerCore/Asm/z2mario/metasprite.s | 89 ++++----- RandomizerCore/Asm/z2r.inc | 3 + 5 files changed, 287 insertions(+), 46 deletions(-) delete mode 100644 RandomizerCore/Asm/z2mario/hammer_suit.s diff --git a/RandomizerCore/Asm/z2mario/hammer_suit.s b/RandomizerCore/Asm/z2mario/hammer_suit.s deleted file mode 100644 index e69de29bb..000000000 diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index ac5667b4b..1ab8a07ac 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -2,6 +2,7 @@ .include "z2r.inc" .import GameRoutines, ProcFireball_Bubble, PlayerGfxHandler, DrawMetasprite +.import ProcHammerTime .import Square1SfxHandler, Square2SfxHandler, NoiseSfxHandler .import SwapToSavedPRG, SwapToPRG0 @@ -136,7 +137,7 @@ PatchLinkDrawRoutine: @fireballLoop: ldx ObjectMetasprite,y beq @nofireball - jsr DrawMetasprite + jsr DrawMetasprite @nofireball: inc R2 ldy R2 @@ -358,6 +359,7 @@ NoDecTimers: jsr $903A ; link main ; This has to come before the A/B buttons are switched over + jsr ProcHammerTime jsr ProcFireball_Bubble ;process fireballs and air bubbles lda A_B_Buttons ;save current A and B button @@ -402,8 +404,10 @@ SetPlayerDownstabbingHitbox: PatchFireballHitcheck: jsr $E694 ; original hitbox check bcc @exit - ldy $11 ; fireball + ldy $11 ; fireball or hammer lda #%10000000 + ora Fireball_State,y ; keep the hammer bit as well if its set + and #%11000000 sta Fireball_State,y @exit: rts diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index e89520ada..c2e2ad69e 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -9,6 +9,7 @@ .import METASPRITE_SMALL_MARIO_JUMPING, METASPRITE_SMALL_MARIO_SWIMMING_1_KICK, METASPRITE_SMALL_MARIO_STANDING, METASPRITE_SMALL_MARIO_SKIDDING .import METASPRITE_SMALL_MARIO_WALKING_1, METASPRITE_SMALL_MARIO_CLIMBING_1, METASPRITE_SMALL_MARIO_GROW_STANDING, METASPRITE_FIREBALL_FRAME_1 .import METASPRITE_FIREBALL_FRAME_2, METASPRITE_EXPLOSION_FRAME_1, METASPRITE_EXPLOSION_FRAME_2, METASPRITE_EXPLOSION_FRAME_3 +.import METASPRITE_HAMMER_FRAME_1, METASPRITE_HAMMER_FRAME_2 .import SWIMMING_ANIMATION_FRAME_COUNT .export GameRoutines, ProcFireball_Bubble, PlayerGfxHandler @@ -2423,6 +2424,9 @@ FireballObjCore: lda Fireball_State,x ;check for d7 = 1 asl bcs FireballExplosion ;if so, branch to get relative coordinates and draw explosion + ; And check if its a player hammer (bit 6) + asl + bcs NoFBall ldy Fireball_State,x ;if fireball inactive, branch to leave beq NoFBall dey ;if fireball state set to 1, skip this part and just run it @@ -2445,6 +2449,10 @@ FireballObjCore: sta Fireball_X_Speed,x lda #4 ;set vertical speed of fireball sta Fireball_Y_Speed,x + + lda #0 ; Don't add extra gravity ? + sta SprObject_X_MoveForce + FireballOffset,x + sta SprObject_Y_MoveForce + FireballOffset,x ; lda #$07 ; sta Fireball_BoundBoxCtrl,x ;set bounding box size control for fireball dec Fireball_State,x ;decrement state to 1 to skip this part from now on @@ -2487,6 +2495,9 @@ NoFBall: rts ;leave FireballExplosion: + ; And check if its a player hammer (bit 6) + asl + bcs NoFBall jsr RelativeFireballPosition jmp DrawExplosion_Fireball @@ -2698,6 +2709,228 @@ GetProperObjOffset: tax ;put back in X and leave rts +.export ProcHammerTime +.proc ProcHammerTime + lda $078b ; Check for hammer in inventory + beq UpdateHammers + ; and that the player is pushing up + b + lda SavedJoypadBits + and #B_Button | Up_Dir ;check for up + b button pressed + cmp #B_Button | Up_Dir + bne UpdateHammers ;branch if not pressed + and #B_Button ;check if b button just pressed + and PreviousA_B_Buttons + bne UpdateHammers ;if button pressed in previous frame, branch + lda FireballCounter ;load fireball counter + and #%00000001 ;get LSB and use as offset for buffer + tax + ; Check to see if we have an open fireball spot to put the hammer in + lda Fireball_State,x ;load fireball state + bne UpdateHammers ;if not inactive, branch + ldy Player_Y_HighPos ;if player too high or too low, branch + dey + bne UpdateHammers + inc FireballCounter ;increment fireball counter + ; Spawn new hammer + lda #$41 + sta Fireball_State,x + + lda #Sfx_Fireball ;play fireball sound effect + sta Square1SoundQueue + + lda Player_X_Position ;get player's horizontal position + sta Fireball_X_Position,x + lda Player_PageLoc ;get player's page location + sta Fireball_PageLoc,x + lda Player_Y_Position ;get player's vertical position and store + sec + sbc #$10 + sta Fireball_Y_Position,x + lda #$01 ;set high byte of vertical position + sbc #0 + sta Fireball_Y_HighPos,x + ldy PlayerFacingDir ;get player's facing direction + tya ; Z2 needs a value set for the knockback value in $6d + sta Fireball_FacingDir,x + dey ;decrement to use as offset here + lda HammerXVelocity,y ;set horizontal speed of hammer + clc + adc Player_X_Speed ; add the player speed to the hammer + sta Fireball_X_Speed,x + lda #-3 ;set vertical speed of hammer + sta Fireball_Y_Speed,x + + lda #0 ; Don't add extra gravity ? + sta SprObject_X_MoveForce + FireballOffset,x + sta SprObject_Y_MoveForce + FireballOffset,x + ; Clear frame count + sta PlayerProj_Cnt,x + +UpdateHammers: + bit Fireball_State + bvc NotHammer1 + ldx #0 + ; Process hammer 1 + jsr MoveHammer +NotHammer1: + bit Fireball_State+1 + bvc NotHammer2 + ldx #1 + jmp MoveHammer +NotHammer2: + rts + +HammerXVelocity: + .byte $10, -$10 +.endproc + +.proc MoveHammer + lda Fireball_State,x ;check for d7 = 1 + asl + bcs Erase ;if so, branch to get relative coordinates and draw explosion + +; stx ObjectOffset +; txa ;add 7 to offset to use +; clc ;as fireball offset for next routines +; adc #FireballOffset +; tax +; lda #$50 ;set downward movement force here +; sta $00 +; lda #8 ;set maximum speed here +; sta $02 +; lda #$00 +; jsr ImposeGravity ;do sub here to impose gravity on fireball and move vertically + + ; Hammer specific Y velocity code + ldy #0 ; use Y as the hibit + lda Fireball_Y_Speed,x + bpl + + ; speed is negative, so dey to sign extend + dey + + + clc + adc Fireball_Y_Position,x + sta Fireball_Y_Position,x + tya + adc Fireball_Y_HighPos,x + sta Fireball_Y_HighPos,x + ; Check if the hammer is offscreen quickly + cmp #2 + bcs Erase + + ; Hammer specific X velocity code + ldy #0 + lda Fireball_X_Speed,x + asl + asl + asl + asl + adc SprObject_X_MoveForce,x + sta SprObject_X_MoveForce,x + + php + lda Fireball_X_Speed,x + lsr + lsr + lsr + lsr + cmp #%00001000 ; Check the sign bit + bcc Positive ; If the value was not negatively signed, jump ahead + ora #%11110000 ; Otherwise, apply a sign extension + dey +Positive: + plp ; Restore carry bit + + adc Fireball_X_Position,x + sta Fireball_X_Position,x + tya + adc Fireball_PageLoc,x + sta Fireball_PageLoc,x + + ; Every 8 frames, increase the Y velocity by one + inc PlayerProj_Cnt,x + lda PlayerProj_Cnt,x + and #$07 + bne DontInc + inc Fireball_Y_Speed,x ; Increase Y velocity (gravity) +DontInc: + +; jsr MoveObjectHorizontally ;do another sub to move it horizontally + +; ldx ObjectOffset +;; Quick check if we went offscreen vertically +; lda Fireball_Y_HighPos,x +; cmp #2 +; bcs Erase + ; don't bother doing horizontal offscreen check :p +; ldy #3 +; jsr $F27D ; run the vanilla z2 offscreen check +; ; load the offscreen bits for this enemy +; lda $cb +; and #$FC +; bne Erase + jmp DrawHammer +Erase: + lda #$00 ;erase hammer state + sta Fireball_State,x + sta FireballMetasprite,x + rts + +.endproc + +OAM_FLIP_V = %10000000 +OAM_FLIP_H = %01000000 + +.proc DrawHammer + lda #0 + sta Fireball_SprAttrib,x + lda #1 + sta Fireball_FacingDir,x + ldy #METASPRITE_HAMMER_FRAME_1 + lda TimerControl + bne ForceHPose + lda Fireball_State,x ;otherwise get hammer's state + and #%00111111 ;mask out d7 + cmp #$01 ;check to see if set to 1 yet + beq GetHPose ;if so, branch +ForceHPose: + lda #0 + beq RenderH ;do unconditional branch to rendering part +GetHPose: + lda FrameCounter ;get frame counter + lsr ;move d3-d2 to d1-d0 + lsr + and #%00000011 ;mask out all but d1-d0 (changes every four frames) +RenderH: + sta R2 + ; if bit 0 is set, then use the horizontal hammer sprite + lsr + bcc CheckForVerticalFlip + ldy #METASPRITE_HAMMER_FRAME_2 +CheckForVerticalFlip: + lsr + bcc WriteMetasprite + ; if bit 1 is also set, then apply vertical flip too + ; but if only bit 2 is set, apply the horizontal flag + lda R2 + lsr + bcs :+ + lda Fireball_SprAttrib,x + ora #OAM_FLIP_H + sta Fireball_SprAttrib,x + : + lda Fireball_SprAttrib,x + ora #OAM_FLIP_V + sta Fireball_SprAttrib,x + + ; and use the sprite that faces the other way + lda #2 + sta Fireball_FacingDir,x +WriteMetasprite: + tya + sta FireballMetasprite,x + rts +.endproc ; .proc GetXOffscreenBits ; stx R4 ;save position in buffer to here ; ldy #$01 ;start with right side of screen diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index 3be547882..191a2464c 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -954,20 +954,20 @@ MetaspriteBox "SMALL_FIRE", "SWIMMING_STILL_2", $00, $02, $28, $2a ; Update this with the first and last metasprite if more are added before or after TOTAL_MARIO_METASPRITES = METASPRITE_SMALL_FIRE_SWIMMING_STILL_2 - METASPRITE_BIG_MARIO_STANDING + 1 -POWERUP_VRAM_OFFSET = SPRITE_BANK_1 -; POWERUP_Y_OFFSET = 8 -POWERUP_1UP_PALETTE = $01 -POWERUP_STAR_PALETTE = $00 -POWERUP_FIREFLOWER_PALETTE = $00 -POWERUP_FIREFLOWER_Y_OFFSET = -8 -POWERUP_MUSHROOM_PALETTE = $02 -POWERUP_1UP_NO_MIRROR = 1 -POWERUP_MUSHROOM_NO_MIRROR = 1 -MetaspriteBox "POWERUP", "STAR", $34, $36 -; Force the lower two parts of the fireflower to be palette two by using the override -MetaspriteBox "POWERUP", "FIREFLOWER", $38, $38 | SPR_FLIP_H , $3a | SPR_PALETTE_1, $3a | SPR_FLIP_H | SPR_PALETTE_1 -MetaspriteBox "POWERUP", "MUSHROOM", $3c, $3e -MetaspriteBox "POWERUP", "1UP", $3c, $3e +;POWERUP_VRAM_OFFSET = SPRITE_BANK_1 +;; POWERUP_Y_OFFSET = 8 +;POWERUP_1UP_PALETTE = $01 +;POWERUP_STAR_PALETTE = $00 +;POWERUP_FIREFLOWER_PALETTE = $00 +;POWERUP_FIREFLOWER_Y_OFFSET = -8 +;POWERUP_MUSHROOM_PALETTE = $02 +;POWERUP_1UP_NO_MIRROR = 1 +;POWERUP_MUSHROOM_NO_MIRROR = 1 +;MetaspriteBox "POWERUP", "STAR", $34, $36 +;; Force the lower two parts of the fireflower to be palette two by using the override +;MetaspriteBox "POWERUP", "FIREFLOWER", $38, $38 | SPR_FLIP_H , $3a | SPR_PALETTE_1, $3a | SPR_FLIP_H | SPR_PALETTE_1 +;MetaspriteBox "POWERUP", "MUSHROOM", $3c, $3e +;MetaspriteBox "POWERUP", "1UP", $3c, $3e FIREBALL_VRAM_OFFSET = SPRITE_BANK_0 FIREBALL_PALETTE = $00 @@ -984,34 +984,34 @@ MetaspriteBox "EXPLOSION", "FRAME_2", $28, $28 | SPR_FLIP_H MetaspriteBox "EXPLOSION", "FRAME_3", $2a, $2a | SPR_FLIP_H ; Firebars will still draw Fireballs manually for now -COIN_VRAM_OFFSET = SPRITE_BANK_1 -COIN_PALETTE = $02 -MetaspriteBox "COIN", "FRAME_1", $2c -MetaspriteBox "COIN", "FRAME_2", $2e -MetaspriteBox "COIN", "FRAME_3", $30 -MetaspriteBox "COIN", "FRAME_4", $32 +;COIN_VRAM_OFFSET = SPRITE_BANK_1 +;COIN_PALETTE = $02 +;MetaspriteBox "COIN", "FRAME_1", $2c +;MetaspriteBox "COIN", "FRAME_2", $2e +;MetaspriteBox "COIN", "FRAME_3", $30 +;MetaspriteBox "COIN", "FRAME_4", $32 -MISC_VRAM_OFFSET = SPRITE_BANK_1 -MISC_PALETTE = $03 -MISC_Y_OFFSET = -8 +;MISC_VRAM_OFFSET = SPRITE_BANK_1 +;MISC_PALETTE = $03 +;MISC_Y_OFFSET = -8 ; The brick and block sprite palettes are set by the code -MISC_BRICK_GROUND_PALETTE = $00 -MISC_BRICK_OTHER_PALETTE = $00 ; The brick palette is set by the code -MISC_BLOCK_PALETTE = $00 ; The block palette is set by the code -MetaspriteBox "MISC", "BRICK_OTHER", $20, $20 -MetaspriteBox "MISC", "BRICK_GROUND", $22, $22 -MetaspriteBox "MISC", "BLOCK", $24, $24 | SPR_FLIP_H +;MISC_BRICK_GROUND_PALETTE = $00 +;MISC_BRICK_OTHER_PALETTE = $00 ; The brick palette is set by the code +;MISC_BLOCK_PALETTE = $00 ; The block palette is set by the code +;MetaspriteBox "MISC", "BRICK_OTHER", $20, $20 +;MetaspriteBox "MISC", "BRICK_GROUND", $22, $22 +;MetaspriteBox "MISC", "BLOCK", $24, $24 | SPR_FLIP_H ; drawn manually for now until i figure out if i have frames for the brick ; break animation with just metasprites ; MetaspriteBox "MISC", "CHUNK", $16 -MISC_BUBBLE_PALETTE = $02 -MetaspriteBox "MISC", "BUBBLE", $18 -MetaspriteBox "MISC", "SMALL_OVERLAY", $1e, $1e -MetaspriteBox "MISC", "LARGE_OVERLAY", $1e, $1e, $1e, $1e -MISC_STAR_FLAG_VRAM_OFFSET = SPRITE_BANK_2 -MISC_STAR_FLAG_PALETTE = $02 -MetaspriteBox "MISC", "STAR_FLAG", $3c, $3e +;MISC_BUBBLE_PALETTE = $02 +;MetaspriteBox "MISC", "BUBBLE", $18 +;MetaspriteBox "MISC", "SMALL_OVERLAY", $1e, $1e +;MetaspriteBox "MISC", "LARGE_OVERLAY", $1e, $1e, $1e, $1e +;MISC_STAR_FLAG_VRAM_OFFSET = SPRITE_BANK_2 +;MISC_STAR_FLAG_PALETTE = $02 +;MetaspriteBox "MISC", "STAR_FLAG", $3c, $3e ; MISC_FLAGPOLE_FLAG_VRAM_OFFSET = SPRITE_BANK_3 ; MISC_FLAGPOLE_FLAG_PALETTE = $01 ; MetaspriteBox "MISC", "FLAGPOLE_FLAG", $32, $34 @@ -1104,14 +1104,15 @@ MetaspriteBox "MISC", "STAR_FLAG", $3c, $3e ; MetaspriteBox "BLOOPER", "SWIM_1", $1a, $1a | SPR_FLIP_H, $3a, $3a | SPR_FLIP_H ; MetaspriteBox "BLOOPER", "SWIM_2", $18, $18 | SPR_FLIP_H -; HAMMER_VRAM_OFFSET = SPRITE_BANK_1 -; HAMMER_FRAME_1_X_OFFSET = 4 -; HAMMER_FRAME_1_Y_OFFSET = -8 -; HAMMER_FRAME_2_X_OFFSET = 0 -; HAMMER_FRAME_2_Y_OFFSET = -8 -; HAMMER_PALETTE = $03 -; MetaspriteBox "HAMMER", "FRAME_1", $1e -; MetaspriteBox "HAMMER", "FRAME_2", $00, $02 + HAMMER_FRAME_1_VRAM_OFFSET = SPRITE_BANK_2 + HAMMER_FRAME_2_VRAM_OFFSET = SPRITE_BANK_0 + HAMMER_FRAME_1_X_OFFSET = 4 + HAMMER_FRAME_1_Y_OFFSET = -8 + HAMMER_FRAME_2_X_OFFSET = 0 + HAMMER_FRAME_2_Y_OFFSET = -8 + HAMMER_PALETTE = $01 + MetaspriteBox "HAMMER", "FRAME_1", $18 + MetaspriteBox "HAMMER", "FRAME_2", $1c, $1e ; HAMMER_BRO_VRAM_OFFSET = SPRITE_BANK_3 ; HAMMER_BRO_PALETTE = $01 diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 30138889b..fa6b32275 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -251,6 +251,8 @@ RESV PlayerChrBank RESV ReloadCHRBank RESV ObjectVerticalFlip RESV Local_eb +; Used by hammers to track how long they've been alive +RESV PlayerProj_Cnt, 2 ; Values use for SMB1 sfx audio engine @@ -495,6 +497,7 @@ SET_RES_BASE $6FB0 RESV SprObject_SprAttrib, 16 Player_SprAttrib = SprObject_SprAttrib Fireball_SprAttrib = SprObject_SprAttrib + FireballOffset +Fireball_FacingDir = PlayerFacingDir + FireballOffset RESV Player_YMoveForceFractional, 16 From 7b28389b5cfefa31f89216cb43e08dea7862160f Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 16 Feb 2026 17:17:34 -0500 Subject: [PATCH 010/128] Fix hammer to actually work properly --- CommandLine/DefaultPlayerOptions.json | 2 +- CommandLine/Program.cs | 4 ++-- RandomizerCore/Asm/z2mario/integration.s | 11 +++++++---- RandomizerCore/Asm/z2mario/mario.s | 16 ++++++++++------ RandomizerCore/Asm/z2r.inc | 1 + 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CommandLine/DefaultPlayerOptions.json b/CommandLine/DefaultPlayerOptions.json index f9ab2bbf6..4ca1038b6 100644 --- a/CommandLine/DefaultPlayerOptions.json +++ b/CommandLine/DefaultPlayerOptions.json @@ -1,6 +1,6 @@ { "DisableMusic": false, - "RandomizeMusic": true, + "RandomizeMusic": false, "MixCustomAndOriginalMusic": false, "IncludeDiverseMusic": false, "DisableUnsafeMusic": true, diff --git a/CommandLine/Program.cs b/CommandLine/Program.cs index b6c645011..3e3db67ee 100644 --- a/CommandLine/Program.cs +++ b/CommandLine/Program.cs @@ -150,7 +150,7 @@ public async Task Randomize() : Directory.GetCurrentDirectory(); } string newFileName = $"{outpath}/Z2_{Seed}_{Flags}.nes"; - await File.WriteAllBytesAsync(newFileName, rom.romdata); + await File.WriteAllBytesAsync(newFileName, rom.romdata!); logger.Info($"File {newFileName} has been created!"); if (rom.debuginfo != null) { @@ -162,7 +162,7 @@ public async Task Randomize() else { logger.Error("An exception occurred generating the rom"); - logger.Error(rom.messages); + logger.Error(rom.messages!); } } diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 1ab8a07ac..0fbd30a6f 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -405,10 +405,13 @@ PatchFireballHitcheck: jsr $E694 ; original hitbox check bcc @exit ldy $11 ; fireball or hammer - lda #%10000000 - ora Fireball_State,y ; keep the hammer bit as well if its set - and #%11000000 - sta Fireball_State,y + ; if its a hammer let it pass through + lda Fireball_State,y + and #%01000000 + bne @exit + ; destroy the fireball + lda #%10000000 + sta Fireball_State,y @exit: rts diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index c2e2ad69e..5590b6266 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -2443,7 +2443,7 @@ FireballObjCore: sta Fireball_Y_HighPos,x ldy PlayerFacingDir ;get player's facing direction tya ; Z2 needs a value set for the knockback value in $6d - sta $6d,x + sta Fireball_MovingDir,x dey ;decrement to use as offset here lda FireballXSpdData,y ;set horizontal speed of fireball accordingly sta Fireball_X_Speed,x @@ -2734,24 +2734,26 @@ GetProperObjOffset: ; Spawn new hammer lda #$41 sta Fireball_State,x - lda #Sfx_Fireball ;play fireball sound effect sta Square1SoundQueue - + + ldy PlayerFacingDir ;get player's facing direction lda Player_X_Position ;get player's horizontal position + clc + adc HammerXPosition-1,y sta Fireball_X_Position,x lda Player_PageLoc ;get player's page location + adc #0 sta Fireball_PageLoc,x lda Player_Y_Position ;get player's vertical position and store sec - sbc #$10 + sbc #$08 sta Fireball_Y_Position,x lda #$01 ;set high byte of vertical position sbc #0 sta Fireball_Y_HighPos,x - ldy PlayerFacingDir ;get player's facing direction tya ; Z2 needs a value set for the knockback value in $6d - sta Fireball_FacingDir,x + sta Fireball_MovingDir,x dey ;decrement to use as offset here lda HammerXVelocity,y ;set horizontal speed of hammer clc @@ -2782,6 +2784,8 @@ NotHammer2: HammerXVelocity: .byte $10, -$10 +HammerXPosition: + .byte $10, $00 .endproc .proc MoveHammer diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index fa6b32275..101203e6b 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -308,6 +308,7 @@ FrameCounter = $12 ; CrouchingFlag = $17 RESV CrouchingFlag Player_MovingDir = $5F +Fireball_MovingDir = $6D PlayerFacingDir = $9F SavedJoypadBits = $F7 ScreenLeft_PageLoc = $72a From c506ce4f60890d204a0d2a8b026416879425205e Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 23 Feb 2026 00:31:27 -0500 Subject: [PATCH 011/128] Make shield spell hold up a real shield --- .../Asm/z2mario/fireball_explosion.chr | Bin 0 -> 128 bytes RandomizerCore/Asm/z2mario/integration.s | 149 ++++- RandomizerCore/Asm/z2mario/mario.s | 76 ++- RandomizerCore/Asm/z2mario/metasprite.s | 588 +----------------- .../Asm/z2mario/sprites_mario_hammer.chr | Bin 4096 -> 4096 bytes RandomizerCore/Asm/z2r.inc | 4 +- RandomizerCore/Hyrule.cs | 5 + RandomizerCore/RandomizerCore.csproj | 2 + 8 files changed, 229 insertions(+), 595 deletions(-) create mode 100644 RandomizerCore/Asm/z2mario/fireball_explosion.chr diff --git a/RandomizerCore/Asm/z2mario/fireball_explosion.chr b/RandomizerCore/Asm/z2mario/fireball_explosion.chr new file mode 100644 index 0000000000000000000000000000000000000000..b4fe4361466eeef074548adb01f2dd616b706265 GIT binary patch literal 128 zcmZQzU|?Y4X5#@;K)}e%!^X`7Vlgvb`Sse1u{cM1Pp)%fdQBUv<=JwN&^6KoC~1< literal 0 HcmV?d00001 diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 0fbd30a6f..f2d467791 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -11,6 +11,9 @@ ; Remove unused code after we gutted link's movement FREE "PRG0" [$92BF, $962D) +; Clear out the old flame / sword projectile code +FREE "PRG0" [$9815, $9925) + ; .segment "PRG0" ; .org (($10ea - $10) .mod $4000) + $8000 ; .byte $17 @@ -120,6 +123,10 @@ PatchLinkDrawRoutine: beq @skipplayer ldy #0 jsr DrawMetasprite + lda $070f ; nonzero if shield is cast + beq + + jsr DrawShieldSprite + + ; Update the player bank if the metasprite has changed .import PlayerBankTable ldy ObjectMetasprite @@ -148,6 +155,64 @@ PatchLinkDrawRoutine: pla rts +.reloc +ShieldSpriteXOffset: + .byte 24 ; Overlaps with Y offset table to save a byte +ShieldSpriteYOffset: + .byte 0, 16 +.reloc +.proc DrawShieldSprite +.import MetaspriteTableLeftLo, MetaspriteTableLeftHi, MetaspriteTableRightLo, MetaspriteTableRightHi +.import MetaspriteRenderLoop, METASPRITE_MARIO_SHIELD +Ptr = R0 +; OrigOffset = R2 +Atr = R3 +Xlo = R4 +Xhi = R5 +Ylo = R6 +Yhi = R7 + lda PlayerFacingDir + lsr + tay + lda MetaspriteTableLeftLo + METASPRITE_MARIO_SHIELD + sta Ptr + lda MetaspriteTableLeftHi + METASPRITE_MARIO_SHIELD + sta Ptr+1 + lda Player_X_Position + sec + sbc ScreenLeft_X_Pos + sta Xlo + lda SprObject_PageLoc + sbc ScreenLeft_PageLoc + sta Xhi + lda Xlo + clc + adc ShieldSpriteXOffset,y + sta Xlo + lda #0 + adc Xhi + sta Xhi + lda Player_Y_HighPos + sta Yhi + lda $17 ; If crouching $17 == 0 + eor #1 ; so set it to 1 if its not + ora PlayerSize ; Or if we are small + tax ; then we want to move the shield Y pos down 16 + lda SprObject_Y_Position + clc + adc ShieldSpriteYOffset,x + sta Ylo + ; use the facing dir as the HFlip bit + cpy #1 + bcc FacingRight + lda #1 + .byte $2c ; bit ABS to skip two bytes +FacingRight: + lda #%01000001 + sta Atr + jmp MetaspriteRenderLoop +.endproc + .org $88AF jmp OverworldLateInit .reloc @@ -313,7 +378,8 @@ LinkGainedHPOrMagic: jsr PatchLinkMain .reloc PatchLinkMain: - ldx #$00 + ldx #$00 + stx JustThrownHammer ; Clear the just thrown hammer flag stx $14 lda $de ; check if we can move beq @notfrozen @@ -402,7 +468,17 @@ SetPlayerDownstabbingHitbox: .segment "PRG7" .reloc PatchFireballHitcheck: - jsr $E694 ; original hitbox check + ldy $11 ; fireball or hammer + ; if its a hammer let it pass through + lda Fireball_State,y + and #%01000000 + beq @IsFireball + ; Is Hammer + jsr $e67e ; original hitbox check for swords (skipping past sword cooldown check) + jmp @AfterCheck +@IsFireball: + jsr $E694 ; original hitbox check for projectiles +@AfterCheck: bcc @exit ldy $11 ; fireball or hammer ; if its a hammer let it pass through @@ -415,6 +491,17 @@ PatchFireballHitcheck: @exit: rts +; Skip over stopping the projectile +.org $E646 + jmp *+3 + +; This location is called when a projectile hits an enemy thats immune to projectil +.org $e6e6 ; Exploding the sword beam (and stopping its movement) + rts ; we can just skip this part since we don't care. + ; If it was a hammer we want it to pass through. If it was a fireball, it'll die in + ; the patch check above +FREE_UNTIL $e6f3 + ; .org $E558 ; rts ; disable link shield maybe? ; .org $E563 @@ -814,3 +901,61 @@ DrawMapMario: sta Sprite_X_Position+12 rts + +.segment "PRG7" +.org $d29c + jsr ClearExtraRAMOnScreenTransition +.reloc +ClearExtraRAMOnScreenTransition: + ; original patched code + lda #0 + tax + ; also clear custom projectile state + sta Fireball_State+0 + sta Fireball_State+1 + rts + +; Give mario a shield hitbox only if he has cast shield +.org LoadPlayerShieldCollisionBox + jsr CheckIfShieldIsCast +.reloc +CheckIfShieldIsCast: + ldy $070F ; non zero if shield is cast + beq @ShieldNotCast + ; shield is cast so perform regular check + ldy PlayerFacingDir + dey + rts +@ShieldNotCast: + ; give the shield an impossibly small hitbox in the corner of the screen + ldy #0 + sty $00 + sty $01 + sty $02 + sty $03 + ; double return to skip the original shield code + pla + pla + rts +.org $e97e ; In the middle of Link hitbox loading fun + jsr CheckIfPlayerSmall + nop +.org $e9e8 ; In the middle of the shield hitbox loading func + jsr CheckIfPlayerSmall + nop +.reloc +CheckIfPlayerSmall: + sta R2 + lda $17 ; If crouching $17 == 0 + eor #1 ; flip it after so that its checking if tall mario + ora PlayerSize ; Or if we are small + eor #1 ; flip it again + tay ; then we want to move the shield Y pos down 16 + rts + +; Update links crouching hitbox to match mario's small size +.org $e971 + .byte $10 ; small Y offset + .byte $00 ; big Y offset + .byte $0f ; small Y length + .byte $1d ; big Y length \ No newline at end of file diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 5590b6266..42f71e4e0 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -2386,29 +2386,39 @@ ProcFireball_Bubble: beq ProcFireballs ;branch if not pressed and PreviousA_B_Buttons bne ProcFireballs ;if button pressed in previous frame, branch - lda FireballCounter ;load fireball counter - and #%00000001 ;get LSB and use as offset for buffer - tax - lda Fireball_State,x ;load fireball state - bne ProcFireballs ;if not inactive, branch + + lda JustThrownHammer + bne ProcFireballs +; lda FireballCounter ;load fireball counter +; and #%00000001 ;get LSB and use as offset for buffer +; tax +; lda Fireball_State,x ;load fireball state +; bne ProcFireballs ;if not inactive, branch ldy Player_Y_HighPos ;if player too high or too low, branch dey bne ProcFireballs + lda CrouchingFlag ;if player crouching, branch bne ProcFireballs - lda Player_State ;if player's state = climbing, branch - cmp #$03 - beq ProcFireballs - lda #Sfx_Fireball ;play fireball sound effect - sta Square1SoundQueue - lda #$02 ;load state - sta Fireball_State,x - ldy PlayerAnimTimerSet ;copy animation frame timer setting - sty FireballThrowingTimer ;into fireball throwing timer - dey - sty PlayerAnimTimer ;decrement and store in player's animation timer - inc FireballCounter ;increment fireball counter - +; lda Player_State ;if player's state = climbing, branch +; cmp #$03 +; beq ProcFireballs + ldx #0 ;load fireball counter + ; Check to see if we have an open fireball spot to put the hammer in + lda Fireball_State,x ;load fireball state + beq SpawnFireball ;if not inactive, branch + inx + lda Fireball_State,x ;load fireball state + bne ProcFireballs ;if not inactive, branch +SpawnFireball: + lda #Sfx_Fireball ;play fireball sound effect + sta Square1SoundQueue + lda #$02 ;load state + sta Fireball_State,x + ldy PlayerAnimTimerSet ;copy animation frame timer setting + sty FireballThrowingTimer ;into fireball throwing timer + dey + sty PlayerAnimTimer ;decrement and store in player's animation timer ProcFireballs: ldx #$00 jsr FireballObjCore ;process first fireball object @@ -2681,10 +2691,10 @@ DrawExplosion_Fireball: ;fallthrough tay ;use whatever's in A for offset ; prevent rotation of the fireball from bleeding into the explosion -; lda #0 -; sta Fireball_SprAttrib,x - ; lda ExplosionTiles,y ;get tile number using offset - ; sta R0 + lda #1 + sta Fireball_SprAttrib,x + lda ExplosionTiles,y ;get tile number using offset + sta FireballMetasprite,x rts ; jsr GetFBOAMOffset ; ldx R0 @@ -2721,16 +2731,24 @@ GetProperObjOffset: and #B_Button ;check if b button just pressed and PreviousA_B_Buttons bne UpdateHammers ;if button pressed in previous frame, branch - lda FireballCounter ;load fireball counter - and #%00000001 ;get LSB and use as offset for buffer - tax - ; Check to see if we have an open fireball spot to put the hammer in - lda Fireball_State,x ;load fireball state - bne UpdateHammers ;if not inactive, branch ldy Player_Y_HighPos ;if player too high or too low, branch dey bne UpdateHammers - inc FireballCounter ;increment fireball counter + + ldx #0 + ; Check to see if we have an open fireball spot to put the hammer in + lda Fireball_State,x ;load fireball state + beq SpawnHammer ;if not inactive, branch + inx + lda Fireball_State,x ;load fireball state + bne UpdateHammers ;if not inactive, branch +SpawnHammer: + inc JustThrownHammer + ; make mario use the throw animation frame + ldy PlayerAnimTimerSet ;copy animation frame timer setting + sty FireballThrowingTimer ;into fireball throwing timer + dey + sty PlayerAnimTimer ;decrement and store in player's animation timer ; Spawn new hammer lda #$41 sta Fireball_State,x diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index 191a2464c..ae52834cb 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -103,7 +103,7 @@ .segment "PRG0" -.export PlayerBankTable +.export PlayerBankTable, MetaspriteRenderLoop, METASPRITE_MARIO_SHIELD PRINT_METASPRITE_IDS = 0 @@ -364,193 +364,6 @@ Id = .ident(Name) .endmacro - -; .proc DrawAllMetasprites - -; LoopCount = M0 - -; lda #24 - 1 ; size of the different object update list -; sta LoopCount -; lda SpriteShuffleOffset -; clc -; adc #19 -; cmp #24 -; bcc :+ -; ; implicit carry set -; sbc #24 -; : -; sta SpriteShuffleOffset -; lda FrameCounter -; lsr -; lda SpriteShuffleOffset -; bcc ObjectLoopNegative - -; ObjectLoopPositive: -; clc -; adc #13 -; cmp #24 -; bcc :+ -; ; implicit carry set -; sbc #24 -; : -; ; skip index zero since we draw the player first always. -; beq NextLoop -; ; TODO check offscreenbits to make sure they are onscreen still -; tay -; ldx ObjectMetasprite,y -; beq NextLoop -; cpx #METASPRITES_COUNT ; todo remove this after fixing all bugs -; bcs NextLoop -; sta SpriteShuffleOffset -; jsr DrawMetasprite -; lda SpriteShuffleOffset -; NextLoop: -; dec LoopCount -; bpl ObjectLoopPositive -; jmp HandleFloateyNumbers - -; ObjectLoopNegative: -; sec -; sbc #13 -; bcs :+ -; ; implicit carry clear -; adc #24 -; : -; ; skip index zero since we draw the player first always. -; beq NextLoop2 -; ; TODO check offscreenbits to make sure they are onscreen still -; tay -; ldx ObjectMetasprite,y -; beq NextLoop2 -; cpx #METASPRITES_COUNT ; todo remove this after fixing all bugs -; bcs NextLoop2 -; sta SpriteShuffleOffset -; jsr DrawMetasprite -; lda SpriteShuffleOffset -; NextLoop2: -; dec LoopCount -; bpl ObjectLoopNegative - -; HandleFloateyNumbers: -; sta SpriteShuffleOffset - -; ldx #7-1 -; FloateyNumberLoop: -; lda FloateyNum_Control,x ;load control for floatey number -; beq Skip ;if zero, branch to leave -; phx -; jsr FloateyNumberRender -; plx -; Skip: -; dex -; bpl FloateyNumberLoop - - ; lda CurrentOAMOffset - ; pha - - ; ; put the player in slot 0 always - ; lda #0 - ; sta CurrentOAMOffset - - ; ; draw the player first so it doesn't ever flicker - ; ; first clear out the sprites that are reserved for the player - ; lda #$f8 - ; sta Sprite_Y_Position + 0 - ; sta Sprite_Y_Position + 4 - ; sta Sprite_Y_Position + 8 - ; sta Sprite_Y_Position + 12 - -; .if ::MOUSE_DISPLAY_CURSOR -; sta Sprite_Y_Position + 16 -; sta Sprite_Y_Position + 20 -; lda mouse -; bmi connected -; ; not connected so draw the alternate sprite at the middle of the screen -; lda #256 / 2 - 8 -; sta mouse + kMouseX -; lda #240 / 2 - 8 -; sta mouse + kMouseY - -; lda FrameCounter -; and #%00100000 -; beq useothersprite ; switch between connected and not connected -; ldy #1 -; bne loadmetasprite ;unconditional -; useothersprite: -; ldy #0 -; beq loadmetasprite -; connected: -; ldy MouseState -; loadmetasprite: -; ldx MouseStateMetasprite,y -; lda MetaspriteTableLeftLo,x -; sta R0 ; Ptr -; lda MetaspriteTableLeftHi,x -; sta R1 ; Ptr -; lda #0 -; sta R3 ; Atr -; lda mouse + kMouseX -; sta R4 ; Xlo -; lda #0 -; sta R5 ; Xhi -; lda mouse + kMouseY -; sta R6 ; Ylo -; lda #1 -; sta R7 ; Yhi -; jsr MetaspriteRenderLoop -; .endif - - ; ldy #0 - ; ldx ObjectMetasprite,y - ; ; unless the player is currently flickering due to damage taken - ; beq DoneDrawingPlayer - ; ; Load the bank for the player if it changed - ; lda PlayerBankTable,x - ; cmp PlayerChrBank - ; beq :+ - ; sta PlayerChrBank - ; inc ReloadCHRBank - ; : - ; jsr DrawMetasprite - ; DoneDrawingPlayer: - - ; Clear sprites up to the offset - ; Calculate CurrentOAMOffset / 4 * 3 and add that to the OAM clear to jump - ; the middle of the clear routine based on how many we need to clear - ; pla ; CurrentOAMOffset - ; lsr - ; sta M0 - ; lsr - ; ; clc - always cleared - ; adc M0 - ; ; clc - always cleared - ; adc #OAMClear - ; adc #0 - ; sta M1 - ; lda #$f8 - ; jmp (M0) - -; .if ::MOUSE_DISPLAY_CURSOR -; MouseStateMetasprite: -; .byte METASPRITE_MOUSE_POINTER -; .byte METASPRITE_MOUSE_DISCONNECTED -; .byte METASPRITE_MOUSE_PINCH -; .endif -; .endproc - -; MoveAllSpritesOffscreen: -; ldy #0 -; sty CurrentOAMOffset -; lda #$f8 -; OAMClear: -; .repeat 64, I -; sta Sprite_Y_Position + I*4 ; write 248 into OAM data's Y coordinate -; .endrepeat -; rts - - .proc DrawMetasprite Ptr = R0 ; OrigOffset = R2 @@ -628,7 +441,7 @@ SetYOffset: ; lda #OAM_FLIP_V ; .byte $2c ; masks the next lda #0 ; DontSetFlipBit: - lda #0 + lda #0 ora SprObject_SprAttrib,y sta Atr @@ -778,53 +591,6 @@ NoPaletteBitMask: .byte (SPR_NO_PALETTE >> 8) .endproc -; ;------------------------------------------------------------------------------------- -; .proc FloateyNumberRender -; Ptr = R0 -; ; OrigOffset = R2 -; Atr = R3 -; Xlo = R4 -; Xhi = R5 -; Ylo = R6 -; Yhi = R7 - -; lda FloateyNum_Y_Pos,x ;get vertical coordinate for -; cmp #$18 ;floatey number, if coordinate in the -; bcc SetupNumSpr ;status bar, branch -; sbc #$01 -; sta FloateyNum_Y_Pos,x ;otherwise subtract one and store as new -; SetupNumSpr: -; sta Ylo -; lda #1 -; sta Yhi -; lda #0 -; sta Atr -; sta Xhi -; lda FloateyNum_X_Pos,x -; sta Xlo -; ldy FloateyNum_Control,x -; ldx FloateyNumMetasprites-1,y -; lda MetaspriteTableRightLo,x -; sta Ptr -; lda MetaspriteTableRightHi,x -; sta Ptr+1 -; jmp MetaspriteRenderLoop -; ; implicit rts - -; FloateyNumMetasprites: -; .byte METASPRITE_NUMBER_100 -; .byte METASPRITE_NUMBER_200 -; .byte METASPRITE_NUMBER_400 -; .byte METASPRITE_NUMBER_500 -; .byte METASPRITE_NUMBER_800 -; .byte METASPRITE_NUMBER_1000 -; .byte METASPRITE_NUMBER_2000 -; .byte METASPRITE_NUMBER_4000 -; .byte METASPRITE_NUMBER_5000 -; .byte METASPRITE_NUMBER_8000 -; .byte METASPRITE_NUMBER_1UP - -; .endproc .export DrawMetasprite @@ -954,23 +720,8 @@ MetaspriteBox "SMALL_FIRE", "SWIMMING_STILL_2", $00, $02, $28, $2a ; Update this with the first and last metasprite if more are added before or after TOTAL_MARIO_METASPRITES = METASPRITE_SMALL_FIRE_SWIMMING_STILL_2 - METASPRITE_BIG_MARIO_STANDING + 1 -;POWERUP_VRAM_OFFSET = SPRITE_BANK_1 -;; POWERUP_Y_OFFSET = 8 -;POWERUP_1UP_PALETTE = $01 -;POWERUP_STAR_PALETTE = $00 -;POWERUP_FIREFLOWER_PALETTE = $00 -;POWERUP_FIREFLOWER_Y_OFFSET = -8 -;POWERUP_MUSHROOM_PALETTE = $02 -;POWERUP_1UP_NO_MIRROR = 1 -;POWERUP_MUSHROOM_NO_MIRROR = 1 -;MetaspriteBox "POWERUP", "STAR", $34, $36 -;; Force the lower two parts of the fireflower to be palette two by using the override -;MetaspriteBox "POWERUP", "FIREFLOWER", $38, $38 | SPR_FLIP_H , $3a | SPR_PALETTE_1, $3a | SPR_FLIP_H | SPR_PALETTE_1 -;MetaspriteBox "POWERUP", "MUSHROOM", $3c, $3e -;MetaspriteBox "POWERUP", "1UP", $3c, $3e - FIREBALL_VRAM_OFFSET = SPRITE_BANK_0 -FIREBALL_PALETTE = $00 +FIREBALL_PALETTE = $01 FIREBALL_X_OFFSET = 8 FIREBALL_Y_OFFSET = -12 EXPLOSION_VRAM_OFFSET = SPRITE_BANK_1 @@ -979,332 +730,43 @@ EXPLOSION_X_OFFSET = 8 EXPLOSION_Y_OFFSET = -12 ; Vanilla offsets the explosion by -4. The other -8 is to account for position due to 8x16 sprites? MetaspriteBox "FIREBALL", "FRAME_1", $3c MetaspriteBox "FIREBALL", "FRAME_2", $3e -MetaspriteBox "EXPLOSION", "FRAME_1", $26, $26 | SPR_FLIP_H -MetaspriteBox "EXPLOSION", "FRAME_2", $28, $28 | SPR_FLIP_H -MetaspriteBox "EXPLOSION", "FRAME_3", $2a, $2a | SPR_FLIP_H -; Firebars will still draw Fireballs manually for now - -;COIN_VRAM_OFFSET = SPRITE_BANK_1 -;COIN_PALETTE = $02 -;MetaspriteBox "COIN", "FRAME_1", $2c -;MetaspriteBox "COIN", "FRAME_2", $2e -;MetaspriteBox "COIN", "FRAME_3", $30 -;MetaspriteBox "COIN", "FRAME_4", $32 - -;MISC_VRAM_OFFSET = SPRITE_BANK_1 -;MISC_PALETTE = $03 -;MISC_Y_OFFSET = -8 - -; The brick and block sprite palettes are set by the code -;MISC_BRICK_GROUND_PALETTE = $00 -;MISC_BRICK_OTHER_PALETTE = $00 ; The brick palette is set by the code -;MISC_BLOCK_PALETTE = $00 ; The block palette is set by the code -;MetaspriteBox "MISC", "BRICK_OTHER", $20, $20 -;MetaspriteBox "MISC", "BRICK_GROUND", $22, $22 -;MetaspriteBox "MISC", "BLOCK", $24, $24 | SPR_FLIP_H -; drawn manually for now until i figure out if i have frames for the brick -; break animation with just metasprites -; MetaspriteBox "MISC", "CHUNK", $16 -;MISC_BUBBLE_PALETTE = $02 -;MetaspriteBox "MISC", "BUBBLE", $18 -;MetaspriteBox "MISC", "SMALL_OVERLAY", $1e, $1e -;MetaspriteBox "MISC", "LARGE_OVERLAY", $1e, $1e, $1e, $1e -;MISC_STAR_FLAG_VRAM_OFFSET = SPRITE_BANK_2 -;MISC_STAR_FLAG_PALETTE = $02 -;MetaspriteBox "MISC", "STAR_FLAG", $3c, $3e -; MISC_FLAGPOLE_FLAG_VRAM_OFFSET = SPRITE_BANK_3 -; MISC_FLAGPOLE_FLAG_PALETTE = $01 -; MetaspriteBox "MISC", "FLAGPOLE_FLAG", $32, $34 - -; PLATFORM_VRAM_OFFSET = SPRITE_BANK_1 -; PLATFORM_PALETTE = $02 -; MetaspriteReserve "PLATFORM_GIRDER_SMALL" -; MetaspriteReserve "PLATFORM_GIRDER_SMALL_FRAME_2" -; MetaspriteReserve "PLATFORM_GIRDER_LARGE" -; MetaspriteReserve "PLATFORM_GIRDER_LARGE_FRAME_2" -; MetaspriteReserve "PLATFORM_CLOUD_SMALL" -; MetaspriteReserve "PLATFORM_CLOUD_SMALL_FRAME_2" -; MetaspriteReserve "PLATFORM_CLOUD_LARGE" -; MetaspriteReserve "PLATFORM_CLOUD_LARGE_FRAME_2" - -; NUMBER_VRAM_OFFSET = SPRITE_BANK_1 -; NUMBER_PALETTE = $02 -; NUMBER_Y_OFFSET = -16 - -; MetaspriteBox "NUMBER", "100", $06, $10 -; MetaspriteBox "NUMBER", "200", $08, $10 -; MetaspriteBox "NUMBER", "400", $0a, $10 -; MetaspriteBox "NUMBER", "500", $0c, $10 -; MetaspriteBox "NUMBER", "800", $0e, $10 -; MetaspriteBox "NUMBER", "1000", $06, $04 -; MetaspriteBox "NUMBER", "2000", $08, $04 -; MetaspriteBox "NUMBER", "4000", $0a, $04 -; MetaspriteBox "NUMBER", "5000", $0c, $04 -; MetaspriteBox "NUMBER", "8000", $0e, $04 -; MetaspriteBox "NUMBER", "1UP", $12, $14 - - -; GOOMBA_VRAM_OFFSET = SPRITE_BANK_2 -; GOOMBA_PALETTE = $03 -; GOOMBA_DEAD_Y_OFFSET = $08 -; MetaspriteBox "GOOMBA", "WALKING_1", $22, $24 -; MetaspriteBox "GOOMBA", "WALKING_2", $24 | SPR_FLIP_H, $22 | SPR_FLIP_H -; MetaspriteBox "GOOMBA", "DEAD", $26, $26 | SPR_FLIP_H - - -; KOOPA_VRAM_OFFSET = SPRITE_BANK_2 -; KOOPA_Y_OFFSET = -8 -; KOOPA_PALETTE = $00 ; Don't set a palette here so we can do red or green koopa later -; ; Koopa shell is offset by 2 pixels for the right side up animation. -; ; We account for this in the code by adding 2 with the vertical flip flag -; KOOPA_SHELL_Y_OFFSET = 0 -; KOOPA_SHELL_REVIVE_Y_OFFSET = 0 -; MetaspriteBox "KOOPA", "WALKING_1", $38, $12, $30, $32 -; MetaspriteBox "KOOPA", "WALKING_2", $18, $16, $34, $36 -; MetaspriteBox "KOOPA", "SHELL", $1a, $1a | SPR_FLIP_H -; MetaspriteBox "KOOPA", "SHELL_REVIVE", $3a, $3a | SPR_FLIP_H -; MetaspriteBox "KOOPA", "FLYING_1", $10, $12, $30, $32 -; MetaspriteBox "KOOPA", "FLYING_2", $14, $16, $34, $36 - - -; PIRANHA_VRAM_OFFSET = SPRITE_BANK_3 -; PIRANHA_PALETTE = $01 -; PIRANHA_Y_OFFSET = -8 -; MetaspriteBox "PIRANHA", "MOUTH_OPEN", $1c, $1c | SPR_FLIP_H, $3c, $3c | SPR_FLIP_H -; MetaspriteBox "PIRANHA", "MOUTH_CLOSED", $1e, $1e | SPR_FLIP_H, $3e, $3e | SPR_FLIP_H - -; BULLET_VRAM_OFFSET = SPRITE_BANK_2 -; BULLET_PALETTE = $03 -; MetaspriteBox "BULLET", "BILL", $1c, $1e - -; PODOBOO_VRAM_OFFSET = SPRITE_BANK_4 -; PODOBOO_PALETTE = $02 -; MetaspriteBox "PODOBOO", "UP", $18, $18 | SPR_FLIP_H - - -; BUZZY_BEETLE_VRAM_OFFSET = SPRITE_BANK_2 -; BUZZY_BEETLE_PALETTE = $03 -; ; See note about koopa shell for this offset -; BUZZY_BEETLE_SHELL_Y_OFFSET = 0 -; MetaspriteBox "BUZZY_BEETLE", "WALKING_1", $00, $02 -; MetaspriteBox "BUZZY_BEETLE", "WALKING_2", $04, $06 -; MetaspriteBox "BUZZY_BEETLE", "SHELL", $20, $20 | SPR_FLIP_H - - -; CHEEP_CHEEP_VRAM_OFFSET = SPRITE_BANK_2 -; CHEEP_CHEEP_PALETTE = $00 -; MetaspriteBox "CHEEP_CHEEP", "SWIM_1", $2c, $2e -; MetaspriteBox "CHEEP_CHEEP", "SWIM_2", $2a, $2e - - -; BLOOPER_VRAM_OFFSET = SPRITE_BANK_3 -; BLOOPER_PALETTE = $03 -; BLOOPER_Y_OFFSET = -8 -; BLOOPER_SWIM_2_Y_OFFSET = (-8 + 3) ; original code adds 3 px to the y position -; MetaspriteBox "BLOOPER", "SWIM_1", $1a, $1a | SPR_FLIP_H, $3a, $3a | SPR_FLIP_H -; MetaspriteBox "BLOOPER", "SWIM_2", $18, $18 | SPR_FLIP_H - - HAMMER_FRAME_1_VRAM_OFFSET = SPRITE_BANK_2 - HAMMER_FRAME_2_VRAM_OFFSET = SPRITE_BANK_0 - HAMMER_FRAME_1_X_OFFSET = 4 - HAMMER_FRAME_1_Y_OFFSET = -8 - HAMMER_FRAME_2_X_OFFSET = 0 - HAMMER_FRAME_2_Y_OFFSET = -8 - HAMMER_PALETTE = $01 - MetaspriteBox "HAMMER", "FRAME_1", $18 - MetaspriteBox "HAMMER", "FRAME_2", $1c, $1e - -; HAMMER_BRO_VRAM_OFFSET = SPRITE_BANK_3 -; HAMMER_BRO_PALETTE = $01 -; HAMMER_BRO_Y_OFFSET = -8 -; MetaspriteBox "HAMMER_BRO", "WALK_1", $00, $02, $20, $22 -; MetaspriteBox "HAMMER_BRO", "WALK_2", $08, $0a, $24, $26 -; MetaspriteBox "HAMMER_BRO", "THROW_1", $04, $06, $20, $22 -; MetaspriteBox "HAMMER_BRO", "THROW_2", $04, $06, $24, $26 - - -; LAKITU_VRAM_OFFSET = SPRITE_BANK_3 -; LAKITU_PALETTE = $01 -; LAKITU_NORMAL_Y_OFFSET = -16 -; LAKITU_THROWING_Y_OFFSET = 0 -; MetaspriteBox "LAKITU", "NORMAL", $0c, $0e, $2c, $2e -; MetaspriteBox "LAKITU", "THROWING", $30, $30 | SPR_FLIP_H - - -; SPINY_VRAM_OFFSET = SPRITE_BANK_2 -; SPINY_PALETTE = $02 -; SPINY_EGG_1_VRAM_OFFSET = SPRITE_BANK_3 -; SPINY_EGG_2_VRAM_OFFSET = SPRITE_BANK_3 -; MetaspriteBox "SPINY", "WALK_1", $08, $0a -; MetaspriteBox "SPINY", "WALK_2", $0c, $0e -; MetaspriteBox "SPINY", "EGG_1", $10, $10 | SPR_FLIP_H | SPR_FLIP_V -; MetaspriteBox "SPINY", "EGG_2", $12, $12 | SPR_FLIP_H | SPR_FLIP_V - - -; JUMPSPRING_VRAM_OFFSET = SPRITE_BANK_3 -; JUMPSPRING_PALETTE = $02 -; JUMPSPRING_Y_OFFSET = -8 -; MetaspriteBox "JUMPSPRING", "FRAME_1", $16, $16 | SPR_FLIP_H , $36, $36 | SPR_FLIP_H -; MetaspriteBox "JUMPSPRING", "FRAME_2", $38, $38 | SPR_FLIP_H -; MetaspriteBox "JUMPSPRING", "FRAME_3", $14, $14 - -; BOWSER_VRAM_OFFSET = SPRITE_BANK_4 -; BOWSER_PALETTE = $01 -; BOWSER_Y_OFFSET = -8 -; MetaspriteBox "BOWSER", "FRONT_MOUTH_OPEN", $08, $0a, $28 -; MetaspriteBox "BOWSER", "FRONT_MOUTH_CLOSED", $0c, $0e, $28 -; MetaspriteBox "BOWSER", "REAR_WALK_1", $04, $06, $20, $22 -; MetaspriteBox "BOWSER", "REAR_WALK_2", $04, $06, $24, $26 - -; MetaspriteReserve "BOWSER_FLAME" - -; PEACH_VRAM_OFFSET = SPRITE_BANK_4 -; PEACH_PALETTE = $02 -; PEACH_Y_OFFSET = -8 -; MetaspriteBox "PEACH", "STANDING", $14, $16, $34, $34 | SPR_FLIP_H -; TOAD_VRAM_OFFSET = SPRITE_BANK_4 -; TOAD_PALETTE = $02 -; TOAD_Y_OFFSET = -8 -; MetaspriteBox "TOAD", "STANDING", $10, $10 | SPR_FLIP_H, $12, $12 | SPR_FLIP_H - -; .if ::MOUSE_DISPLAY_CURSOR -; MOUSE_VRAM_OFFSET = SPRITE_BANK_5 -; MOUSE_PALETTE = $02 -; MOUSE_NO_MIRROR = 1 -; MOUSE_Y_OFFSET = -8 -; MetaspriteBox "MOUSE", "POINTER", $00, $02 -; MetaspriteBox "MOUSE", "PINCH", $04, $06 -; MetaspriteBox "MOUSE", "DISCONNECTED", $08, $0a -; .endif - - -; Extra sprite tiles that aren't using metasprites - -; These aren't actually sprite tiles, they are the replacement tile for -; the rope when using a balance platform -BALANCE_PLATFORM_ROPE_1 = $7d -BALANCE_PLATFORM_ROPE_2 = $7e - -; Brick chunks have a really custom movement and making them a metasprite -; wasn't worth the trouble for now. -BRICK_CHUNK_TILE = MetaspriteVramOffset {$16}, {SPRITE_BANK_1} - -; Used by the "small platform" which is the elevator platforms -; PLATFORM_GIRDER = $5b -PLATFORM_GIRDER = MetaspriteVramOffset {$1a}, {SPRITE_BANK_1} - -; Vine drawing is probably possible to do as a metasprite but not worth the trouble -VINE_TILE_1 = MetaspriteVramOffset {$28}, {SPRITE_BANK_3} -VINE_TILE_2 = MetaspriteVramOffset {$2a}, {SPRITE_BANK_3} - -; Used when drawing firebars (not when shooting fireballs which are metasprites) -FIREBALL_TILE1 = MetaspriteVramOffset {$3c}, {SPRITE_BANK_0} -FIREBALL_TILE2 = MetaspriteVramOffset {$3e}, {SPRITE_BANK_0} - +; The Explosion CHR is pasted over part of links downstab animation +MetaspriteBox "EXPLOSION", "FRAME_1", $00, $00 | SPR_FLIP_H +MetaspriteBox "EXPLOSION", "FRAME_2", $02, $02 | SPR_FLIP_H +MetaspriteBox "EXPLOSION", "FRAME_3", $04, $04 | SPR_FLIP_H + + +HAMMER_FRAME_1_VRAM_OFFSET = SPRITE_BANK_2 +HAMMER_FRAME_2_VRAM_OFFSET = SPRITE_BANK_0 +HAMMER_FRAME_1_X_OFFSET = 4 +HAMMER_FRAME_1_Y_OFFSET = -8 +HAMMER_FRAME_2_X_OFFSET = 0 +HAMMER_FRAME_2_Y_OFFSET = -8 +HAMMER_PALETTE = $01 +MetaspriteBox "HAMMER", "FRAME_1", $18 +MetaspriteBox "HAMMER", "FRAME_2", $1c, $1e + +MARIO_SHIELD_VRAM_OFFSET = SPRITE_BANK_1 +MARIO_SHIELD_Y_OFFSET = -8 +MARIO_SHIELD_PALETTE = $00 +MetaspriteBox "MARIO", "SHIELD", $06 ;;;;;;;;;;; ; NULL Metasprite needs to be reserved in slot 0 to allow disabling drawing a sprite before its deleted MetaspriteData "METASPRITE_NULL", $0000 -;;;;;;;;;;; -; Girder Platform - -; Y_OFFSET .set 0 -; X_OFFSET .set 0 -; PALETTE .set 2 - -; MetaspriteData "METASPRITE_PLATFORM_GIRDER_SMALL", MetaspritePlatformGirderSmall -; MetaspriteData "METASPRITE_PLATFORM_GIRDER_SMALL_FRAME_2", MetaspritePlatformGirderSmallFrame2 - -; MetaspritePlatformGirderSmall: -; .byte 4 * 4 -; .repeat 4, I -; .byte MetaspriteVramOffset{$1a}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (I * 8) -; .endrepeat - -; MetaspritePlatformGirderSmallFrame2: -; .byte 4 * 4 -; .repeat 4, I -; .byte MetaspriteVramOffset{$1a}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (((I + 2) .mod 4) * 8) -; .endrepeat - -; MetaspriteData "METASPRITE_PLATFORM_GIRDER_LARGE", MetaspritePlatformGirderLarge -; MetaspriteData "METASPRITE_PLATFORM_GIRDER_LARGE_FRAME_2", MetaspritePlatformGirderLargeFrame2 - -; MetaspritePlatformGirderLarge: -; .byte 6 * 4 -; .repeat 6, I -; .byte MetaspriteVramOffset{$1a}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (I * 8) -; .endrepeat - -; MetaspritePlatformGirderLargeFrame2: -; .byte 6 * 4 -; .repeat 6, I -; .byte MetaspriteVramOffset{$1a}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (((I + 3) .mod 6) * 8) -; .endrepeat - -; ;;;;;;;;;;; -; ; Cloud Platform - -; Y_OFFSET .set 0 -; X_OFFSET .set 0 -; PALETTE .set 2 - -; MetaspriteData "METASPRITE_PLATFORM_CLOUD_SMALL", MetaspritePlatformCloudSmall -; MetaspriteData "METASPRITE_PLATFORM_CLOUD_SMALL_FRAME_2", MetaspritePlatformCloudSmallFrame2 - -; MetaspritePlatformCloudSmall: -; .byte 4 * 4 -; .repeat 4, I -; .byte MetaspriteVramOffset{$1c}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (I * 8) -; .endrepeat - -; MetaspritePlatformCloudSmallFrame2: -; .byte 4 * 4 -; .repeat 4, I -; .byte MetaspriteVramOffset{$1c}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (((I + 2) .mod 4) * 8) -; .endrepeat - -; MetaspriteData "METASPRITE_PLATFORM_CLOUD_LARGE", MetaspritePlatformCloudLarge -; MetaspriteData "METASPRITE_PLATFORM_CLOUD_LARGE_FRAME_2", MetaspritePlatformCloudLargeFrame2 - -; MetaspritePlatformCloudLarge: -; .byte 6 * 4 -; .repeat 6, I -; .byte MetaspriteVramOffset{$1c}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (I * 8) -; .endrepeat - -; MetaspritePlatformCloudLargeFrame2: -; .byte 6 * 4 -; .repeat 6, I -; .byte MetaspriteVramOffset{$1c}, {SPRITE_BANK_1}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET + (((I + 3) .mod 6) * 8) -; .endrepeat - - -; Y_OFFSET .set -4 -; X_OFFSET .set 0 -; PALETTE .set 2 -; MetaspriteData "METASPRITE_BOWSER_FLAME", MetaspriteBowserFlame, MetaspriteBowserFlame -; MetaspriteBowserFlame: -; .byte 3 * 4 -; .byte MetaspriteVramOffset{$2d}, {SPRITE_BANK_4}, PALETTE, 0 + Y_OFFSET, 0 + X_OFFSET -; .byte MetaspriteVramOffset{$2f}, {SPRITE_BANK_4}, PALETTE, 0 + Y_OFFSET, 8 + X_OFFSET -; .byte MetaspriteVramOffset{$31}, {SPRITE_BANK_4}, PALETTE, 0 + Y_OFFSET, 16 + X_OFFSET - - - - ;;;; ; Internal use ; Generate the lookup tables for the metasprites used by the renderer. ; All metasprites must be defined before this part - +.export MetaspriteTableLeftLo, MetaspriteTableLeftHi, MetaspriteTableRightLo, MetaspriteTableRightHi MetaspriteTableLeftLo: .repeat METASPRITES_COUNT, I .byte .ident(.sprintf("METASPRITE_LEFT_%d_LO", I)) .endrepeat + MetaspriteTableLeftHi: .repeat METASPRITES_COUNT, I .byte .ident(.sprintf("METASPRITE_LEFT_%d_HI", I)) diff --git a/RandomizerCore/Asm/z2mario/sprites_mario_hammer.chr b/RandomizerCore/Asm/z2mario/sprites_mario_hammer.chr index 3c6ff14d401efab905f9ff745546ec606c41875f..d5448d7355e6f2a2afc53640d004f4053fcb5578 100644 GIT binary patch delta 223 zcmZorXi(U2lvyv$`zr$oNZ9aAjP6qaG5J_PBm~6hbz4GMENrRXX`4SYUm{%(I~jVo TFELw_<~AFUJLPO7z|I8#aBL(x delta 223 zcmZorXi(U2lv%H?{5Jy#NZ9brkMC0eG5J_PBm~6h_1i;OENs=~b(=pkUm{%(I~jVo TFELw_<~AFUJLPO7z|I8##7ieS diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 101203e6b..058bf4d33 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -128,6 +128,8 @@ MulHiReg = $5206 NewSideviewInit = $8cec ClearNametables = $d266 JumpEngine = $d385 +LoadPlayerShieldCollisionBox = $e9d8 +ObjectCollisionCheck = $e9f9 ; These are new variables that use free memory Z2Noise_SfxLenCounter = $07f5 @@ -177,7 +179,7 @@ RESV mapMarioPrevDirection ;;; ; MARIO related values that are not directly mapped to vanilla z2 object values -RESV FireballCounter +RESV JustThrownHammer ; RESV ObjectOffset ObjectOffset = $10 RESV FBall_OffscreenBits diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index fc87247b4..84e7a9984 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2422,6 +2422,11 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra rom.Put(ROM.ChrRomOffset + 0x01000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.randomizer_text.chr")); if (props.MarioMode) { + // Overwrite link's downstab animation with fireball explosion sprites + var fireballExplosion = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.fireball_explosion.chr"); + for (var i = 0; i < 13; i++) + rom.Put(ROM.ChrRomOffset + 0x400 + (i * 0x2000), fireballExplosion); + rom.Put(ROM.ChrRomOffset + 0x1a800, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.sprites_mario_hammer.chr")); var span = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.map_mario.chr"); rom.Put(ROM.ChrRomOffset + 0x11a00, span[0..0x220]); diff --git a/RandomizerCore/RandomizerCore.csproj b/RandomizerCore/RandomizerCore.csproj index 10da275ca..728bf979f 100644 --- a/RandomizerCore/RandomizerCore.csproj +++ b/RandomizerCore/RandomizerCore.csproj @@ -68,6 +68,7 @@ + @@ -85,5 +86,6 @@ + From e324c82f2167607789e9e49bab460f717ed28930 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 28 Feb 2026 17:35:47 -0500 Subject: [PATCH 012/128] Break blocks with hammer --- RandomizerCore/Asm/MMC5.s | 15 +++ RandomizerCore/Asm/ram.inc | 1 + RandomizerCore/Asm/z2mario/integration.s | 143 ++++++++++++++++++----- RandomizerCore/Asm/z2mario/mario.s | 4 +- RandomizerCore/Asm/z2r.inc | 18 ++- 5 files changed, 146 insertions(+), 35 deletions(-) diff --git a/RandomizerCore/Asm/MMC5.s b/RandomizerCore/Asm/MMC5.s index 7ee140bae..14b94f079 100644 --- a/RandomizerCore/Asm/MMC5.s +++ b/RandomizerCore/Asm/MMC5.s @@ -802,3 +802,18 @@ LoadAreaBGMetatile: SetupScrollSplitEarly: jsr SetupScanlineIRQ jmp SwapToPRG0 + +; Update the number of slots available for breaking bricks from 5 to BREAKABLE_BLOCK_COUNT +; Not really related to MMC5, but whatever. +.segment "PRG0", "PRG7" +.org $a6d2 + ldx #BREAKABLE_BLOCK_COUNT-1 ; was ldx #4 to search through the brick slots +.org $E292 + ldx #BREAKABLE_BLOCK_COUNT-1 ; was ldx #4 to search through the brick slots +UPDATE_REFS BlockBreakState @ $A6DE $A6E3 $A6EF $E108 $E246 $E295 +UPDATE_REFS BlockBreakMapPage @ $A745 $E127 $E25D +UPDATE_REFS BlockBreakXCoord @ $A752 $E120 $E258 +UPDATE_REFS BlockBreakYCoord @ $A74D $E116 $E251 +UPDATE_REFS BlockBreakColumnOffset @ $A6FC $E2A2 +UPDATE_REFS BlockBreakAddrLo @ $A6F2 $E2A7 +UPDATE_REFS BlockBreakAddrHi @ $A6F7 $E2AC \ No newline at end of file diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index 0b0d108c0..35e98e605 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -260,6 +260,7 @@ EncounterSpawnTimer = $0516 RNG = $051b MapNumber = $0561 TownNumber = $056b +PalaceNumber = $056c PalaceRegionIndex = $056c ; Palace index in continent (0-2) LinkYVelocityHi = $057d Enemy0YVelocity = $057e diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index f2d467791..bc12b1d18 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -104,9 +104,21 @@ PatchLinkDeathSprite: ; patch the link draw routine to skip drawing him with the vanilla code .org $EC02 ; ldx $11 lda $29,x - jsr PatchLinkDrawRoutine + jsr BankPatchLinkDrawRoutine nop +.segment "PRG7" +.reloc +BankPatchLinkDrawRoutine: + jsr SwapToPRG0 + jsr PatchLinkDrawRoutine + jsr SwapToSavedPRG + ; double return to skip the OG drawing code + pla + pla + rts + +.segment "PRG0", "PRG7" .reloc PatchLinkDrawRoutine: ldx $11 @@ -114,7 +126,6 @@ PatchLinkDrawRoutine: lda $29,x rts + - ; put the player in slot 4 always lda #4 sta CurrentOAMOffset @@ -137,7 +148,6 @@ PatchLinkDrawRoutine: inc ReloadCHRBank + @skipplayer: - ; While we are here, lets draw the fireball/hammer sprites too ldy #ProjectileOffset sty $02 @@ -150,9 +160,6 @@ PatchLinkDrawRoutine: ldy R2 cpy #ProjectileOffset+2 ; Draw two fireballs and one hammer bne @fireballLoop - ; double return to skip the OG drawing code - pla - pla rts .reloc @@ -228,26 +235,24 @@ OverworldLateInit: UpdatePlayerPalette: inc ReloadCHRBank ; Just reload the CHR bank to be safe here ; Check fire state and update the palette if we are fire still - ldx #0 - lda #7 - sta R7 + ldx #255 lda $76f and #$10 beq + - ldx #7 + ldx #7-1 + ; we are still firey so write a palette update for next NMI ldy $301 + dey - - lda PlayerRegularPalettePPUCommand,x - sta $302,y iny inx - dec R7 - bne - + lda PlayerRegularPalettePPUCommand,x + sta $301+1,y + bpl - tya clc - adc #7 + adc #6 sta $301 rts PlayerRegularPalettePPUCommand: @@ -449,11 +454,11 @@ SetPlayerDownstabbingHitbox: JSR $ECEA CLC ADC #$25 - 4 ; - 2 cause I don't know - STA $0480 ; ,x + STA HitboxYCoord ; ,x LDA $CC ; ,x CLC ADC #$10 ; + 8 ; + 8 cause UGH - STA $047E ; ,x + STA HitboxXCoord ; ,x + rts @@ -463,9 +468,36 @@ SetPlayerDownstabbingHitbox: .org $E273 jmp $E28B +.segment "PRG7" + +LoadProjectileCollisionBox = $e4bc +SwordCollisionCheck = $E677 +BreakBlockCollisionCheck = $e1e6 +.org $e1e0 + ; Check the hammer hitboxes as if they were stabbing blocks + jsr CheckHammerHitboxes +.reloc +CheckHammerHitboxes: + lda #0 + sta $0b + ldy #1 + jsr LoadProjectileCollisionBox + lda R0 + sta HitboxXCoord + lda R1 + sta HitboxYCoord + jsr BreakBlockCollisionCheck + ldy #0 + jsr LoadProjectileCollisionBox + lda R0 + sta HitboxXCoord + lda R1 + sta HitboxYCoord + jmp BreakBlockCollisionCheck + +; Rewrite the projectile check so that it loops for each projectile and checks hitboxes with each ene .org $E4A8 jsr PatchFireballHitcheck -.segment "PRG7" .reloc PatchFireballHitcheck: ldy $11 ; fireball or hammer @@ -474,20 +506,25 @@ PatchFireballHitcheck: and #%01000000 beq @IsFireball ; Is Hammer - jsr $e67e ; original hitbox check for swords (skipping past sword cooldown check) - jmp @AfterCheck -@IsFireball: - jsr $E694 ; original hitbox check for projectiles -@AfterCheck: - bcc @exit - ldy $11 ; fireball or hammer - ; if its a hammer let it pass through - lda Fireball_State,y - and #%01000000 - bne @exit - ; destroy the fireball - lda #%10000000 - sta Fireball_State,y + ; Load the X / Y coord into $47e and $480 + ; use a "sword" type of attack for the hitbox check + lda #0 + .byte $2c ; skips 2 bytes +; jsr $e67e ; original hitbox check for swords (skipping past sword offscreen check) +; jmp @AfterCheck + @IsFireball: + lda #1 + sta $0b ; Holds the "type" of attack (0 = sword, 1 = projectile) + jsr $E694 ; original hitbox check for projectiles + bcc @exit + ldy $11 ; fireball or hammer + ; if its a hammer let it pass through + lda Fireball_State,y + and #%01000000 + bne @exit + ; destroy the fireball + lda #%10000000 + sta Fireball_State,y @exit: rts @@ -532,6 +569,26 @@ FREE_UNTIL $e6f3 ; .org $93ba ; rts ; patch the link main routine to skip some unneeded stuff +.segment "PRG0", "PRG7" +; Update the position of the brick to remove the mario collision box too +.org $A700 + jmp ClearSpotInCollisionRam +.reloc +.proc ClearSpotInCollisionRam + ; A is always #$42 here? + sta ($00),y + lda $00 + clc + adc #<(COLLISION_TILES - $6000) + sta $00 + lda $01 + adc #>(COLLISION_TILES - $6000) + sta $01 + lda #0 ; Clear out the collision tile + sta ($00),y + rts +.endproc + ; Patch clearing out the ram for sideviews to also clear our new ram block .org $C58F jsr ClearCollisionRAM @@ -615,6 +672,24 @@ SlightlyModifiedCollisionRoutine: ; nop ; nop +.segment "PRG0", "PRG7" +.org $92A5 + jmp PatchFinishPlayerPaletteCycle +.reloc +PatchFinishPlayerPaletteCycle: + sta $0301 ; write player flash counter + ldy $074B ; flash frame counter ; If its the last frame of the spell cycle + bne + + lda $69DE ; and we aren't using shield spell + cmp #$02 + beq + + ; then update our colors + stx $0301 ; reset the position of the vram buffer write so we don't double write colors + jmp UpdatePlayerPalette ; And then update the player palette appropriately + + + rts + + ; Update palette locations ;.ifdef DEMO_CODE ; UPDATE_BYTE $37 @ $285a $2a0a $40af $40bf $40cf $40df $80af $80bf $80cf $80df $c0af $c0bf $c0cf $c0df $c0ef $100af $100bf $100cf $100df $140af $140bf $140cf $140df $17c19 $1c464 $1c47c @@ -797,6 +872,10 @@ SetLinkRecoil = $e371 rts .assert * <= $E66A +; Disable recoil when hitting enemy with hammers +.org $E66A + rts + ; Make mario bounce off armored enemies .org $E714 jsr CheckForDownstab diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 42f71e4e0..adc83adbe 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -952,7 +952,7 @@ JumpSwimSub: DumpFall: ; Force downstab hitbox?? lda Player_Y_Position - sta $480 + sta HitboxYCoord lda VerticalForceDown ;otherwise dump falling into main fractional sta VerticalForce ProcSwim: @@ -2115,7 +2115,7 @@ LandPlyr: ; sta StompChainCounter ;initialize enemy stomp counter ; cancel downstab hitbox?? lda #$f8 - sta $480 + sta HitboxYCoord lda $cc clc adc #$10 diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 058bf4d33..6facc742d 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -166,8 +166,13 @@ RESV DripperRedCounter RESV PpuCtrlForIrq RESV ScrollPosForIrq RESV IrqSetupAlready +; Expanded the block break list to hold up to 10 bricks at a time instead of just 5 +BREAKABLE_BLOCK_COUNT = 10 +RESV BlockBreakYCoord, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0429 - Y Low byte for collision table +RESV BlockBreakColumnOffset, BREAKABLE_BLOCK_COUNT ; Vanilla addr $042E - Offset in the collision table +RESV BlockBreakAddrLo, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table lo addr +RESV BlockBreakAddrHi, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table hi addr -.assert RES_BASE + RES_OFFSET < $01ad .ifdef ENABLE_Z2_MARIO @@ -395,6 +400,16 @@ DeathMusic = %00000001 .endif ; ENABLE_Z2_MARIO +.assert RES_BASE + RES_OFFSET < $01ad + + +RelocatedRAMBase = $041A +SET_RES_BASE RelocatedRAMBase +RESV BlockBreakState, BREAKABLE_BLOCK_COUNT ; Vanilla addr $041a - bits 0-6 = timer; bit 7 set = step block. $00 means no brick breaking +RESV BlockBreakMapPage, BREAKABLE_BLOCK_COUNT ; Vanilla addr $041F - Nametable select for the PPUADDR +RESV BlockBreakXCoord, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0424 - X Low Byte for collision table +.assert RES_BASE + RES_OFFSET < $043E + ;; Stat tracking variables in SRAM ; This value is the second header byte for file 1 thats unused. @@ -492,6 +507,7 @@ RESV Checkpoint, CHECKPOINT_LEN STAT_TRACKING_SIZE = RES_OFFSET ClearSRAMStart = RES_OFFSET + .assert RES_OFFSET + StatTrackingBase < $7800 .ifdef ENABLE_Z2_MARIO From 6c82116ab7c263efed60daedf82a0453a0df084c Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 28 Feb 2026 18:02:38 -0500 Subject: [PATCH 013/128] Revert to .net8 temporarily --- CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj | 2 +- .../Properties/PublishProfiles/FolderProfile.pubxml | 2 +- Directory.Build.props | 2 +- Statistics/Statistics.csproj | 2 +- Tests/Tests.csproj | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj index 3bbe79176..24ae77f49 100644 --- a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj +++ b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj @@ -1,7 +1,7 @@  Exe - net10.0-browser + net8.0-browser browser-wasm wwwroot\main.js ./_framework diff --git a/CrossPlatformUI.Browser/Properties/PublishProfiles/FolderProfile.pubxml b/CrossPlatformUI.Browser/Properties/PublishProfiles/FolderProfile.pubxml index 056667b1d..f165eeba6 100644 --- a/CrossPlatformUI.Browser/Properties/PublishProfiles/FolderProfile.pubxml +++ b/CrossPlatformUI.Browser/Properties/PublishProfiles/FolderProfile.pubxml @@ -7,7 +7,7 @@ publish\ FileSystem <_TargetId>Folder - net10.0-browser + net8.0-browser browser-wasm true false diff --git a/Directory.Build.props b/Directory.Build.props index bd965de9d..920ee012b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - net10.0 + net8.0 enable 1.0.7 11.3.12 diff --git a/Statistics/Statistics.csproj b/Statistics/Statistics.csproj index 5ed9a1bbc..49d6813ce 100644 --- a/Statistics/Statistics.csproj +++ b/Statistics/Statistics.csproj @@ -1,6 +1,6 @@  - net10.0-windows + net8.0-windows Exe Z2Randomizer.Statistics AnyCPU;x64 diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 905b9b64e..cae44bc06 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,7 +1,7 @@  - net10.0-windows + net8.0-windows enable enable From 3ceef55de1daa83fb7c394e024009a364966f070 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 28 Feb 2026 18:07:35 -0500 Subject: [PATCH 014/128] Fix issue with .net8 sucking --- RandomizerCore/Asm/z2mario/integration.s | 4 ++++ RandomizerCore/Hyrule.cs | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index bc12b1d18..14373a5da 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -126,6 +126,10 @@ PatchLinkDrawRoutine: lda $29,x rts + + lda $752 ; Fetch the "behind" flag from vanilla z2 + and #$20 + ora Player_SprAttrib + sta Player_SprAttrib ; and mix that in with our attributes ; put the player in slot 4 always lda #4 sta CurrentOAMOffset diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 84e7a9984..524b1ae94 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -460,7 +460,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC var rom = await ROMData.ApplyAsm(assembler); if (!rom.success) { - return new RandomizerResult(false, null, null, string.Join(Environment.NewLine, rom.messages)); + return new RandomizerResult(false, null, null, string.Join(Environment.NewLine, rom.messages.Select(x => x.ToString()))); } ROMData = new ROM(rom.romdata); @@ -557,7 +557,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC File.WriteAllText("rooms.log", sb.ToString()); } }*/ - return new RandomizerResult(true, ROMData.rawdata, rom.debugfile, string.Join(Environment.NewLine, rom.messages)); + return new RandomizerResult(true, ROMData.rawdata, rom.debugfile, string.Join(Environment.NewLine, rom.messages.Select(x => x.ToString()))); } catch(Exception e) { From 8ae8c28566d6582a38ae4412ad1d68e4337a34d8 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 28 Feb 2026 23:10:34 -0500 Subject: [PATCH 015/128] Major bug fixing efforts --- RandomizerCore/Asm/FullItemShuffle.s | 4 + .../Asm/z2mario/fireball_explosion.chr | Bin 128 -> 160 bytes RandomizerCore/Asm/z2mario/integration.s | 155 ++++++++++++++---- RandomizerCore/Asm/z2mario/mario.s | 29 ++-- RandomizerCore/Asm/z2mario/metasprite.s | 107 +++++------- RandomizerCore/Asm/z2r.inc | 7 +- RandomizerCore/Hyrule.cs | 1 + 7 files changed, 190 insertions(+), 113 deletions(-) diff --git a/RandomizerCore/Asm/FullItemShuffle.s b/RandomizerCore/Asm/FullItemShuffle.s index 6186f6eea..389b7a255 100644 --- a/RandomizerCore/Asm/FullItemShuffle.s +++ b/RandomizerCore/Asm/FullItemShuffle.s @@ -483,7 +483,11 @@ CommonEnemyTileTable: .byte $C3, $C3 ; Crumble Block 2 .byte $B2, $B2 ; Bot Frame 1 .byte $B4, $B4 ; Bot Frame 2 +.ifdef ENABLE_Z2_MARIO + .byte $48, $48 ; Spike top frame 1 (Relocated to another spot in z2mario) +.else .byte $24, $24 ; Spike top frame 1 +.endif .byte $B0, $B0 ; Spike top frame 2 ItemTileTable: .byte $8C, $F5 ; Candle diff --git a/RandomizerCore/Asm/z2mario/fireball_explosion.chr b/RandomizerCore/Asm/z2mario/fireball_explosion.chr index b4fe4361466eeef074548adb01f2dd616b706265..6c8fc2baba12291519d2af170e8becc4abcba383 100644 GIT binary patch delta 39 hcmZo*T);S?L4g4c2ujQI%h%iU3rfmxG8UMz0RU#R1eE{) delta 6 NcmZ3$*uXfU0RRVX0y6*r diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 14373a5da..1fc04cfc1 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -69,11 +69,20 @@ PatchLinkLivesScreenDraw: lsr ; a = 0 sta SprObject_SprAttrib sta ScreenLeft_X_Pos + sta PlayerSize ; Also make mario big mario in the lives screen lda ScreenLeft_PageLoc sta SprObject_PageLoc jsr BankSwitchMarioCHR jmp $EC02 +.org $CAe9 + jsr AlsoResetPlayerSizeOnContinue +.reloc +AlsoResetPlayerSizeOnContinue: + sta PlayerSize + sta $075c ; original code + rts + ; Jump to our own metasprite setting routine .org $919e jsr SetHurtMetasprite ; Just jumped into lava bro @@ -101,23 +110,62 @@ PatchLinkDeathSprite: inc ReloadCHRBank jmp BankSwitchMarioCHR +.org $8fe3 + jmp *+3 ; Skip duplicate draw??? +.reloc +DrawFallingMarioSprite: +.import METASPRITE_BIG_MARIO_JUMPING, METASPRITE_SMALL_MARIO_JUMPING + lda PlayerSize + beq + ; If we are large + lda #METASPRITE_SMALL_MARIO_JUMPING + .byte $2c ; bit $abs + + + lda #METASPRITE_BIG_MARIO_JUMPING + sta ObjectMetasprite + inc ReloadCHRBank + jsr BankSwitchMarioCHR + ; Skip the banked routine since we are already in PRG0 to prevent a crash + jmp $EC02 ; PatchLinkDrawRoutine Use the original since its a double return +.org $8fee + jsr DrawFallingMarioSprite ; patch the link draw routine to skip drawing him with the vanilla code .org $EC02 ; ldx $11 lda $29,x - jsr BankPatchLinkDrawRoutine + jsr PatchLinkDrawRoutine nop .segment "PRG7" .reloc BankPatchLinkDrawRoutine: + lda $0769 + bne + + jsr PatchLinkDrawRoutine + jmp @Exit + + jsr SwapToPRG0 jsr PatchLinkDrawRoutine jsr SwapToSavedPRG +@Exit: ; double return to skip the OG drawing code pla pla rts +.org $E1A2 + jsr CheckIfAboveScreenForFallingFairyCheck + nop +.reloc +CheckIfAboveScreenForFallingFairyCheck: + ; Set the carry if we are properly below the screen + lda Player_Y_Position + cmp #$e4 + bcc + + ; And also check the Y Hi position to make sure we aren't wrapping above the screen + lda Player_Y_HighPos + cmp #1 + + + rts + .segment "PRG0", "PRG7" .reloc PatchLinkDrawRoutine: @@ -126,10 +174,6 @@ PatchLinkDrawRoutine: lda $29,x rts + - lda $752 ; Fetch the "behind" flag from vanilla z2 - and #$20 - ora Player_SprAttrib - sta Player_SprAttrib ; and mix that in with our attributes ; put the player in slot 4 always lda #4 sta CurrentOAMOffset @@ -164,6 +208,9 @@ PatchLinkDrawRoutine: ldy R2 cpy #ProjectileOffset+2 ; Draw two fireballs and one hammer bne @fireballLoop + ; double return to skip the OG drawing code + pla + pla rts .reloc @@ -238,6 +285,8 @@ OverworldLateInit: .reloc UpdatePlayerPalette: inc ReloadCHRBank ; Just reload the CHR bank to be safe here + txa + pha ; Check fire state and update the palette if we are fire still ldx #255 lda $76f @@ -258,6 +307,8 @@ UpdatePlayerPalette: clc adc #6 sta $301 + pla + tax rts PlayerRegularPalettePPUCommand: .byte $3f, $11, $03, $16, $27, $18, $ff @@ -328,11 +379,11 @@ CheckIfTurningSmall: ; Set player routine to ChangeSize lda #$0a sta GameEngineSubroutine - lda #8 - sta InjuryTimer +; lda #8 +; sta InjuryTimer ; we will unlock the scroll lock after the size change animation finishes + inc ScrollLock lda #1 - sta ScrollLock sta Player_State ; sta PlayerChangeSizeFlag lda #0 @@ -348,13 +399,25 @@ CheckIfTurningSmall: .segment "PRG0", "PRG7" ; Make mario grow when his HP increases enough -.org $d42f ; patches over the sound effect for HP gain - jsr LinkGainedHPOrMagic - nop + +.org $cb23 ; patches over the "set life after level" + jsr LifeOrMagicSetToMax .reloc -LinkGainedHPOrMagic: - lda #$10 - sta $ef ; Set the sfx for gaining life/magic/exp +LifeOrMagicSetToMax: + sta $773,x + jmp CheckIfWeAreBig + +; Patch the "crawl up" life regen instead so it covers collecting a heart container too +.org $d41c + jsr LifeOrMagicSetToMax +;.org $d42f ; patches over the sound effect for HP gain +; jsr LinkGainedHPOrMagic +; nop +.reloc +;LinkGainedHPOrMagic: +; lda #$10 +; sta $ef ; Set the sfx for gaining life/magic/exp +CheckIfWeAreBig: ; if we are small and gaining HP cpx #1 ; IF x = 1 we are increasing HP bne @exit @@ -370,8 +433,8 @@ LinkGainedHPOrMagic: lda #$0a sta GameEngineSubroutine ; we will unlock the scroll lock after the size change animation finishes - lda #1 - sta ScrollLock +; lda #1 + inc ScrollLock lda #0 sta ScrollAmount sta Player_State @@ -404,10 +467,12 @@ PatchLinkMain: ; beq @notinjured ; rts ; @notinjured: -; lda $0503 ; sword slash timer -; bne @notwaiting +; lda $0503 ; sword slash timer / entrance move timer +; beq @notmoving +; ldy $701 +; lda $93b5,y ; rts -; @notwaiting: +; @notmoving: ; decrement timers here cause lazy ; Move the timers ahead by a frame as well lda TimerControl ;if master timer control not set, decrement @@ -442,6 +507,19 @@ NoDecTimers: lda #$00 sta Left_Right_Buttons ;nullify left and right buttons temp variable + ; Check scroll lock. If we are locked in, then prevent leaving the screen + lda ScrollLock + beq @SkipScrollLock + lda Player_X_Position + cmp #3 + bcc @Lock + ; Check for right lock + cmp #$e5 + bcc @SkipScrollLock ; You are still in bounds + @Lock: + .import ImpedePlayerMove + jsr ImpedePlayerMove +@SkipScrollLock: jsr SetPlayerDownstabbingHitbox ; jsr CheckSideviewTransition ; this is already run during collision detection @@ -484,20 +562,31 @@ BreakBlockCollisionCheck = $e1e6 CheckHammerHitboxes: lda #0 sta $0b - ldy #1 - jsr LoadProjectileCollisionBox - lda R0 - sta HitboxXCoord - lda R1 - sta HitboxYCoord - jsr BreakBlockCollisionCheck - ldy #0 - jsr LoadProjectileCollisionBox - lda R0 - sta HitboxXCoord - lda R1 - sta HitboxYCoord - jmp BreakBlockCollisionCheck + bit Fireball_State+1 + bvc + + ldy #1 + jsr LoadProjectileCollisionBox + lda R0 + sta HitboxXCoord + lda R1 + sta HitboxYCoord + jsr BreakBlockCollisionCheck + + + bit Fireball_State + bvc + + ldy #0 + jsr LoadProjectileCollisionBox + lda R0 + sta HitboxXCoord + lda R1 + sta HitboxYCoord + jmp BreakBlockCollisionCheck + + + rts + +; Patch the projectile check to use the new state variable instead of metasprite +.org $e49a + lda Fireball_State,y ; Rewrite the projectile check so that it loops for each projectile and checks hitboxes with each ene .org $E4A8 diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index adc83adbe..a818a93a7 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -689,17 +689,17 @@ EndChgSize: cmp #$c4 ;check again for another specific moment bne ExitChgSize ;and branch to leave if before or after that point ; Z2Mario - clear scroll lock after you change size - lda #0 - sta ScrollLock +; lda #0 + dec ScrollLock jmp DonePlayerTask ;otherwise do sub to init timer control and set routine ExitChgSize: rts ; TODO check this RTS can be removed ;and then leave FinishedInjuryBlink: ; Z2Mario - clear scroll lock after you change size - lda #0 - sta ScrollLock - beq DonePlayerTask +; lda #0 + dec ScrollLock + jmp DonePlayerTask ;------------------------------------------------------------------------------------- ; .reloc PlayerInjuryBlink: @@ -2107,11 +2107,6 @@ LandPlyr: sta Player_Y_Speed ;initialize vertical speed and fractional sta Player_Y_MoveForce ;movement force to stop player's vertical movement - ; Z2 Set Player_CollisionBits to fix elevator -; lda #~$04 -; and Player_CollisionBits -; sta Player_CollisionBits - ; sta StompChainCounter ;initialize enemy stomp counter ; cancel downstab hitbox?? lda #$f8 @@ -2238,6 +2233,7 @@ ExCSM: StopPlayerMove: jmp ImpedePlayerMove ;stop player's movement +.export ImpedePlayerMove .reloc ImpedePlayerMove: lda #$00 ;initialize value here @@ -2722,7 +2718,13 @@ GetProperObjOffset: .export ProcHammerTime .proc ProcHammerTime lda $078b ; Check for hammer in inventory - beq UpdateHammers + bne + + jmp UpdateHammers + + + lda PlayerSize ; Have to be big to throw hammers? + beq + + jmp UpdateHammers + + ; and that the player is pushing up + b lda SavedJoypadBits and #B_Button | Up_Dir ;check for up + b button pressed @@ -2907,7 +2909,7 @@ OAM_FLIP_H = %01000000 lda #0 sta Fireball_SprAttrib,x lda #1 - sta Fireball_FacingDir,x + sta Fireball_MovingDir,x ldy #METASPRITE_HAMMER_FRAME_1 lda TimerControl bne ForceHPose @@ -2947,7 +2949,8 @@ CheckForVerticalFlip: ; and use the sprite that faces the other way lda #2 - sta Fireball_FacingDir,x + sta Fireball_MovingDir,x +; sta Fireball_FacingDir,x WriteMetasprite: tya sta FireballMetasprite,x diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index ae52834cb..8f68e4ee9 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -375,12 +375,19 @@ Yhi = R7 ; LoopCount = M0 ; VFlip = M1 - cpx #METASPRITES_COUNT +; cpx #METASPRITES_COUNT +; bcc + +; rts ; Temp work around to prevent crashing when trying to render bad msprs +; + + + cpx #LAST_MARIO_METASPRITE bcc + - rts ; Temp work around to prevent crashing when trying to render bad msprs + ; I'm not a mario sprite so I need to use the MovingDir instead of FacingDir + lda Player_MovingDir,y + jmp @checkFacing + - lda PlayerFacingDir,y +@checkFacing: lsr bne FacingLeft lda MetaspriteTableRightLo,x @@ -407,32 +414,7 @@ DrawSprite: lda SprObject_Y_HighPos,y sta Yhi -; lda #0 -; sta VFlip -; cpy #7 -; bcs SkipVerticalFlipCheck -; lda ObjectVerticalFlip,y -; bpl NoVFlip -; ; Object has a vertical flip attribute set -; sta VFlip -; NoVFlip: -; ; Object has an added Y offset -; ; The negative flag is bit 5 in our number, so sign extend the value -; and #%00111111 -; cmp #%00100000 -; bcc PositiveYOffset -; ; subtracting a small negative offset here -; ora #%11000000 -; clc -; PositiveYOffset: -; adc SprObject_Y_Position,y -; bcc SetYOffset -; ; If the carry is set, then we need to inc Yhi ... TODO -; ; this probably doesn't handle underflow properly? -; inc Yhi -; bcs SetYOffset ; unconditional -; SkipVerticalFlipCheck: - lda SprObject_Y_Position,y + lda SprObject_Y_Position,y SetYOffset: sta Ylo @@ -445,41 +427,36 @@ SetYOffset: ora SprObject_SprAttrib,y sta Atr - jsr MetaspriteRenderLoop - - -; lda VFlip -; beq DontShiftPositions -; ldy #0 -; lda (Ptr),y -; cmp #8 + 1 -; bcc DontShiftPositions -; cmp #12 -; beq Flip3 -; ; otherwise flip all 4 sprites -; lda Sprite_Tilenumber-4,x ;with first or second row tiles -; pha ;and save tiles to the stack -; lda Sprite_Tilenumber-8,x -; pha -; lda Sprite_Tilenumber-12,x ;exchange third row tiles -; sta Sprite_Tilenumber-4,x ;with first or second row tiles -; lda Sprite_Tilenumber-16,x -; sta Sprite_Tilenumber-8,x -; pla ;pull first or second row tiles from stack -; sta Sprite_Tilenumber-16,x ;and save in third row -; pla -; sta Sprite_Tilenumber-12,x -; rts -; Flip3: -; ; Custom flip code for bowser's front since he is weird. -; lda Sprite_Y_Position-4,x -; pha -; lda Sprite_Y_Position-12,x -; sta Sprite_Y_Position-8,x -; sta Sprite_Y_Position-4,x -; pla -; sta Sprite_Y_Position-12,x -; DontShiftPositions: + txa + pha + jsr MetaspriteRenderLoop + pla + cmp #LAST_MARIO_METASPRITE + bcs @exit + ; We are rendering a mario sprite, so check if we are stuck in the mud + lda $0752 + and #$20 + beq @exit + ; stuck in the mud, so update bottom two sprite + ; x = next oam sprite id, so we can offset by 8 to hit bottom two sprites + ; if we are small then its the most recent two sprites. if we are big then its 4 sprites back + lda PlayerSize + beq + ; If we are large + lda $0202 - 8, x + ora #$20 + sta $0202 - 8, x + lda $0202 - 4, x + ora #$20 + sta $0202 - 4, x + rts + + + lda $0202 - 16, x + ora #$20 + sta $0202 - 16, x + lda $0202 - 12, x + ora #$20 + sta $0202 - 12, x +@exit: rts .endproc @@ -720,6 +697,8 @@ MetaspriteBox "SMALL_FIRE", "SWIMMING_STILL_2", $00, $02, $28, $2a ; Update this with the first and last metasprite if more are added before or after TOTAL_MARIO_METASPRITES = METASPRITE_SMALL_FIRE_SWIMMING_STILL_2 - METASPRITE_BIG_MARIO_STANDING + 1 +LAST_MARIO_METASPRITE = METASPRITES_COUNT + FIREBALL_VRAM_OFFSET = SPRITE_BANK_0 FIREBALL_PALETTE = $01 FIREBALL_X_OFFSET = 8 diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 6facc742d..d26fae9ee 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -210,7 +210,8 @@ RESV EventMusicBuffer ; TODO use the vanilla music queues RESV WarpZoneControl RESV Player_CollisionBits ; Split Z2 and SMB1 collision bits RESV HalfwayPage -RESV SwimmingFlag +;RESV SwimmingFlag +SwimmingFlag = $13 RESV CurrentOAMOffset ; RESV ObjectMetasprite RESV PlayerAnimCtrl @@ -325,7 +326,7 @@ ScreenLeft_X_Pos = $72c ScreenRight_X_Pos = $72d ScreenEdge_X_Pos = ScreenLeft_X_Pos -InjuryTimer = $510 +InjuryTimer = $050c NumberofLives = $0700 ; Player_SprAttrib = $0752 ; SprObject_SprAttrib = Player_SprAttrib @@ -516,7 +517,7 @@ SET_RES_BASE $6FB0 RESV SprObject_SprAttrib, 16 Player_SprAttrib = SprObject_SprAttrib Fireball_SprAttrib = SprObject_SprAttrib + FireballOffset -Fireball_FacingDir = PlayerFacingDir + FireballOffset +;Fireball_FacingDir = PlayerFacingDir + FireballOffset RESV Player_YMoveForceFractional, 16 diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 524b1ae94..22bea32e6 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3326,6 +3326,7 @@ private void FullItemShuffle(Assembler asm, IEnumerable nonSideviewLoc a.Set("_REPLACE_FIRE_WITH_DASH", props.ReplaceFireWithDash ? 1 : 0); a.Set("_CHECK_WIZARD_MAGIC_CONTAINER", props.DisableMagicRecs ? 0 : 1); a.Set("_DO_SPELL_SHUFFLE_WIZARD_UPDATE", props.IncludeSpellsInShuffle ? 1 : 0); + a.Assign("ENABLE_Z2_MARIO", props.MarioMode ? 1 : 0); a.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.FullItemShuffle.s"), "full_item_shuffle.s"); } From cdca53504c944571874705c04894b1fe41055043 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 2 Apr 2026 20:45:32 -0400 Subject: [PATCH 016/128] WIP Dark Mario fight --- RandomizerCore/Asm/BuffCarock.s | 1 - RandomizerCore/Asm/MMC5.s | 6 +- RandomizerCore/Asm/z2mario/boss.s | 676 +++++++++++++++ RandomizerCore/Asm/z2mario/boss_reference.md | 852 +++++++++++++++++++ RandomizerCore/Asm/z2mario/integration.s | 25 +- RandomizerCore/Asm/z2mario/mario.s | 2 +- RandomizerCore/Asm/z2mario/metasprite.s | 40 +- RandomizerCore/Asm/z2r.inc | 60 +- RandomizerCore/Hyrule.cs | 2 +- RandomizerCore/ROM.cs | 2 - RandomizerCore/RandomizerCore.csproj | 2 + 11 files changed, 1649 insertions(+), 19 deletions(-) create mode 100644 RandomizerCore/Asm/z2mario/boss.s create mode 100644 RandomizerCore/Asm/z2mario/boss_reference.md diff --git a/RandomizerCore/Asm/BuffCarock.s b/RandomizerCore/Asm/BuffCarock.s index 94b765269..be7d828f2 100644 --- a/RandomizerCore/Asm/BuffCarock.s +++ b/RandomizerCore/Asm/BuffCarock.s @@ -2,7 +2,6 @@ .segment "PRG4" -ProjectileEnemyData = $07c0 ; SinWaveVelocityIncrement = $ba1c ; Hook into carrock's update code to add the new position calculation diff --git a/RandomizerCore/Asm/MMC5.s b/RandomizerCore/Asm/MMC5.s index 14b94f079..afcd6665f 100644 --- a/RandomizerCore/Asm/MMC5.s +++ b/RandomizerCore/Asm/MMC5.s @@ -534,9 +534,9 @@ SwapCHR: adc #1 sta SpChrBank4Reg sta BgChrBank0Reg -.ifdef ENABLE_Z2_MARIO - sta CurrentCHRBank ; Store a copy of the latest BG bank for use in z2mario -.endif +;.ifdef ENABLE_Z2_MARIO +; sta CurrentCHRBank ; Store a copy of the latest BG bank for use in z2mario +;.endif adc #1 sta SpChrBank5Reg sta BgChrBank1Reg diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s new file mode 100644 index 000000000..7114ff2c6 --- /dev/null +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -0,0 +1,676 @@ + + +.include "z2r.inc" + +.import METASPRITE_BIG_MARIO_SKIDDING, METASPRITE_BIG_MARIO_JUMPING, METASPRITE_FIRE_MARIO_STANDING, METASPRITE_BIG_MARIO_STANDING +.import DrawMetasprite +.import SwapPRG, SwapToSavedPRG, SwapToPRG0 + +SKIDDING = METASPRITE_BIG_MARIO_SKIDDING +JUMPING = METASPRITE_BIG_MARIO_JUMPING +SHOOT = METASPRITE_FIRE_MARIO_STANDING +STANDING = METASPRITE_BIG_MARIO_STANDING +mus_bossdie = 8 + +; Boss RAM size for clearing loop +;BOSS_RAM_SIZE = 11 ; boss_animation through boss_freeze (12 bytes, 0-indexed = 11) + +.segment "PRG5", "PRG7" + +; Patch the spot that "kicks" mario shadow out and use it +; as an init routine +.org $985d + jsr PatchBossInit +.reloc +PatchBossInit: + sta EnemyYVelocity + jmp InitShadowBoss + +; Patch the location where it draws the boss +.org $9a7f + jsr BossGraphicsHandler + +.org $9871 + jsr BossLandedInCutscene +.reloc +BossLandedInCutscene: + sta $0505 +.import METASPRITE_BIG_MARIO_CROUCHING + lda #METASPRITE_BIG_MARIO_CROUCHING + sta boss_animation + rts + +.reloc +InitShadowBoss: + ; Clear all boss RAM + lda #0 + ldx #BOSS_RAM_SIZE + @loop: + sta boss_animation,x + dex + bpl @loop + ldx #1 + ; Set HP + lda #20 + sta Enemy_HP,x + lda #JUMPING + sta boss_animation + lda #CHR_BIGMARIO + sta boss_ChrBank + inc ReloadCHRBank + + ; Face left + ldx ObjectOffset + lda #2 + sta Enemy_MovingDir,x + rts + +.reloc +RunBoss: + lda boss_freeze + beq @notfreezed + dec boss_freeze + lda #STANDING + sta boss_animation + ; Draw every other frame (flicker) + lda FrameCounter + lsr + bcs @nodraw2 +; jsr RelativeEnemyPosition + jsr BossGraphicsHandler + @nodraw2: + ldx ObjectOffset + rts + + @notfreezed: + lda Enemy_HP,x + bmi AnimateKilledBoss ; HP < 0: boss is fully dead + bne @alive + jmp KillBoss ; HP == 0: playing death countdown + @alive: + + lda #80 + sta boss_counter ; reset death countdown each frame while alive +; lda #0 +; sta EnemyFrenzyBuffer ; suppress enemy frenzy spawns + + ; Draw (skip every other frame if invincible for blink effect) + lda boss_invincibility_timer + beq @draw + lda FrameCounter + lsr + bcs @nodraw + @draw: +; jsr RelativeEnemyPosition + jsr BossGraphicsHandler + @nodraw: + ldx ObjectOffset + + lda TimerControl + bne @nograv ; skip movement if timer frozen + +; jsr EnemyToBGCollisionDet ; background collision + jsr HandleBossMovement ; AI state machine + + lda Enemy_State,x + and #%01000000 ; check airborne flag + beq @nograv + jsr MoveD_EnemyVertically ; apply gravity + lda #JUMPING + sta boss_animation + @nograv: + + ; Collision detection (only when not invincible) + lda boss_invincibility_timer + bne @decrement_timer +; lda #$0a +; sta Enemy_BoundBoxCtrl,x ; set bounding box size +; jsr GetEnemyOffscreenBits +; jsr GetEnemyBoundBox +; jmp PlayerEnemyCollision ; check player<->boss collision + rts + + @decrement_timer: + dec boss_invincibility_timer + rts +AnimateKilledBoss: + inc $63 + rts + +.reloc +HandleBossMovement: + lda boss_state + jsr JumpEngine + .word MovBossSide ; state 0 + .word MovBossRunning ; state 1 + .word MovBossMiddle ; state 2 + +.reloc +MovBossSide: + lda #STANDING + sta boss_animation + + ; Random fireball: every 8th frame, 1/4 chance + lda FrameCounter + and #%00000111 + bne @no_fireball + lda RNG + and #%00000011 + bne @no_fireball + jsr BossSpawnFireball + @no_fireball: + + ; Random jump: every 8th frame (offset 4), 1/2 chance + lda Enemy_State,x + and #%01000000 + bne @no_jump + lda FrameCounter + and #%00000111 + cmp #4 + bne @no_jump + lda RNG + lsr + bcc @no_jump + lda #<(-5) + sta Enemy_Y_Speed,x + jsr BossJump + @no_jump: + + ; State transition when timer expires + lda boss_state_timer + bne @keep_state + + lda #0 + sta boss_substate + lda FrameCounter + clc + adc Player_X_Position + lsr + lsr + bcs @running + lda #2 ; -> state 2 (middle) + .byte $2c ; skip next instruction (BIT abs trick) + @running: + lda #1 ; -> state 1 (running) + sta boss_state + inc boss_bridge + + @keep_state: + dec boss_state_timer + rts + +.reloc +MovBossRunning: + jsr BossSetWalkingAnim + + lda Enemy_MovingDir,x + cmp #$01 + beq MovBossRight + + ; Moving left + lda Enemy_X_Position,x + cmp #11*16 + bcs @StillAccer + cmp #4*16 + bcs MovStillMoving + @Deaccelerating: + lda #SKIDDING + sta boss_animation + inc Enemy_X_Speed,x + inc Enemy_X_Speed,x + lda Enemy_X_Speed,x + bmi MovStillMoving + jmp MovChangeState + @StillAccer: + dec Enemy_X_Speed,x + dec Enemy_X_Speed,x + jmp MovStillMoving + +MovBossRight: + lda Enemy_X_Position,x + cmp #4*16 + bcc @StillAccer + cmp #11*16 + bcc MovStillMoving + @Deaccelerating: + lda #SKIDDING + sta boss_animation + dec Enemy_X_Speed,x + dec Enemy_X_Speed,x + lda Enemy_X_Speed,x + bpl MovStillMoving + jmp MovChangeState + @StillAccer: + inc Enemy_X_Speed,x + inc Enemy_X_Speed,x + +MovStillMoving: + lda boss_bridge + beq @no_jump + dec boss_bridge + lda Enemy_State,x + and #%01000000 + bne @no_jump + lda #<(-4) + sta Enemy_Y_Speed,x + jsr BossJump + @no_jump: + jmp MoveEnemyHorizontally + +MovChangeState: + lda RNG + and #$f0 + sta boss_state_timer ; random duration for next idle + lda #STANDING + sta boss_animation + lda #0 + sta boss_state ; -> state 0 (side/idle) + lda Enemy_MovingDir,x + eor #%00000011 ; flip direction + sta Enemy_MovingDir,x + rts + +.reloc +MovBossMiddle: + lda boss_substate + beq MovGoToMiddle ; substate 0: walk to center + ; substate 1: middle action (shoot bullets, wait for enemies to die) + + lda #STANDING + sta boss_animation + lda boss_state_timer + bne @not_ready + + ; Timer expired: check if all enemy slots 1-4 are clear + ldy #1 + @loop: + lda Enemy_Flag,y + bne @notyet ; slot still occupied, wait + iny + cpy #5 + bcc @loop + + ; All clear: maybe spawn powerup +; lda Player_State +; cmp #2 +; bcs @no_need_for_p ; player already has fire power +; sta PowerUpType +; lda #PowerUpObject +; cmp Enemy_ID+5 +; beq @no_need_for_p ; powerup already exists +; ; Spawn powerup in slot 5 +; ldy #5 +; lda #PowerUpObject +; sta Enemy_ID,y +; lda Enemy_X_Position,x +; sta Enemy_X_Position+5 +; lda Enemy_Y_Position,x +; sta Enemy_Y_Position+5 +; lda #0 +; sta Enemy_PageLoc+5 +; sta Enemy_X_Speed+5 +; lda #$01 +; sta Enemy_Y_HighPos+5 +; sta Enemy_Flag+5 +; lda #$80 +; sta Enemy_State+5 +; lda #$03 +; sta Enemy_BoundBoxCtrl+5 +; lda #<(-3) +; sta Enemy_Y_Speed+5 + @no_need_for_p: + lda #0 + sta boss_bridge + lda #1 + sta boss_state ; -> state 1 (running) + ; Set speed based on facing direction + lda Enemy_MovingDir,x + lsr + bcs @right + lda #<(-$20) + .byte $2c ; skip next instruction (BIT abs trick) + @right: + lda #$20 + sta Enemy_X_Speed,x + @notyet: + rts + + @not_ready: + dec boss_state_timer + ; Every 16 frames, spawn a bullet bill from a random Y position + lda FrameCounter + and #%00001111 + bne @done + ; Random Y position + lda RNG + and #$70 + clc + adc #$30 + sta $00 ; Y position for bullet + ; Alternate left/right side + lda FrameCounter + and #%00010000 + lsr + lsr + lsr + lsr + clc + adc #1 ; A = 1 or 2 (left or right) + jsr BossSpawnSideBullet + @done: + rts + +MovGoToMiddle: + jsr BossSetWalkingAnim + + ; Target X: $B0 if facing left, $40 if facing right + lda Enemy_MovingDir,x + lsr + bcs @facing_right + ; Facing left: target $B0 + lda Enemy_X_Position,x + cmp #$B0 + bcc @keep_walking + bcs @arrived + @facing_right: + ; Facing right: target $40 + lda Enemy_X_Position,x + cmp #$40 + bcs @arrived + @keep_walking: + ; Set walk speed based on direction + lda Enemy_MovingDir,x + lsr + bcs @walk_right + lda #<(-2) + .byte $2c ; skip next instruction + @walk_right: + lda #2 + sta Enemy_X_Speed,x + jmp MoveEnemyHorizontally + + @arrived: + lda #0 + sta Enemy_X_Speed,x + ; Jump onto center platform + lda Enemy_State,x + and #%01000000 + bne @already_airborne + lda #<(-6) + sta Enemy_Y_Speed,x + jsr BossJump + @already_airborne: + lda #1 + sta boss_substate ; switch to substate 1 + lda #$80 + sta boss_state_timer ; timer for bullet phase + rts + +.reloc +BossSetWalkingAnim: + lda FrameCounter + and #%00000011 + cmp #%00000011 + bne @init_anim + inc boss_walking_anim + lda boss_walking_anim + cmp #$03 + bne @init_anim + lda #0 + sta boss_walking_anim + @init_anim: + lda boss_walking_anim + sta boss_animation + rts + +.reloc +BossJump: + lda #Sfx_BigJump + sta Square1SoundQueue + lda Enemy_State,x + ora #%01000000 ; set airborne flag + sta Enemy_State,x + rts + +.segment "PRG7" +.reloc +BossGraphicsHandler: + rts + +.segment "PRG5", "PRG7" +.reloc +FindID: + ; Find first empty enemy slot (5 down to 1), return in Y. + ; N flag set if none found. + ldy #5 + @loop: + lda Enemy_Flag,y + beq @foundSlot + dey + bne @loop ; stop at 0 (don't use boss slot) + dey ; Y = $FF, N flag set + @foundSlot: + sty $04 + rts + +.reloc +BossSpawnFireball: + jsr FindID ; find empty enemy slot -> Y + bmi @done ; no slot available + lda #Sfx_Fireball + sta Square1SoundQueue + lda #4 + sta boss_shootanim ; trigger shoot animation + lda Enemy_Y_Position,x + clc + adc #4 + sta $00 ; Y pos + lda Enemy_X_Position,x + sta $01 ; X pos + lda #0 + sta $02 ; page + lda #8 + sta $03 ; bounding box size + lda Enemy_MovingDir,x + cmp #$01 + bne @no + pha + lda $01 + clc + adc #8 ; offset X if facing right + sta $01 + pla + @no: + sta Enemy_MovingDir,y + lda #5 ; TODO What to spawn here? BowserFlame + jsr BossSpawnEnemy + ldx ObjectOffset + @done: + rts + +.reloc +BossSpawnSideBullet: + ; A = direction (1=left edge, 2=right edge) + ; $00 = Y position (already set by caller) + sta $02 + jsr FindID + bmi @done + lda $02 + pha + lda #Sfx_Blast + sta Square2SoundQueue + + lda $02 + lsr + bcs @rightdir + + lda #$f0 + sta $01 ; X = left edge ($F0) + lda #<(-60) + sta ProjectileXVelocity,y ; speed = -60 (move right from left) + jmp @rest + + @rightdir: + lda #0 + sta $01 ; X = right edge ($00) + lda #60 + sta ProjectileXVelocity,y ; speed = 60 (move left from right) + + @rest: + lda #8 + sta $03 ; bounding box + lda #0 + sta $02 ; page + + tya + pha + lda #10 ; TODO: what to use to make a bubble? + jsr BossSpawnEnemy + pla + tay + pla + sta Enemy_MovingDir,y + ldx ObjectOffset + @done: + rts + +.reloc +BossSpawnEnemy: + ; Spawn enemy in slot Y with parameters in $00-$03 + ; A = enemy ID +; sta Enemy_ID,y + lda $00 + sta Enemy_Y_Position,y + lda $01 + sta Enemy_X_Position,y + lda $02 + sta Enemy_PageLoc,y +; lda $03 +; sta Enemy_BoundBoxCtrl,y + lda #$01 + sta Enemy_Y_HighPos,y +; sta Enemy_Flag,y + lsr + sta Enemy_X_MoveForce,y + sta Enemy_State,y + rts +; ldx $04 ; slot index +; jmp CheckpointEnemyID ; engine: initialize enemy by ID + +.reloc +KillBoss: + lda boss_counter + bmi @death_complete ; counter expired -> signal fully dead +; lda #Silence +; sta AreaMusicQueue + dec boss_counter + +; jsr EnemyToBGCollisionDet + lda #STANDING + sta boss_animation + lda Enemy_State,x + and #%01000000 + beq @nograv + jsr MoveD_EnemyVertically + lda #JUMPING + sta boss_animation + @nograv: + ; Draw every other frame (flash effect) + lda FrameCounter + lsr + bcs @nodraw +; jsr RelativeEnemyPosition + jsr BossGraphicsHandler + @nodraw: + ldx ObjectOffset + rts + + @death_complete: + ; Counter expired: signal death complete + dec Enemy_HP,x ; HP goes to $FF (negative) +; lda #mus_bossdie +; sta AreaMusicQueue + rts + +;.reloc +;AnimateKilledBoss: +; jsr EraseEnemyObject +; +; lda #3 +; sta pal_type ; set palette type for post-boss +; lda #2 +; sta PowerUpType +; lda #PowerUpObject +; sta Enemy_ID+5 +; lda #0 +; sta Enemy_PageLoc+5 +; sta Enemy_X_Speed+5 +; lda Enemy_X_Position,x +; sta Enemy_X_Position+5 ; spawn at boss's position +; lda #$01 +; sta Enemy_Y_HighPos+5 +; sta Enemy_Flag+5 +; lda Enemy_Y_Position,x +; sta Enemy_Y_Position+5 +; lda #$80 +; sta Enemy_State+5 +; lda #$03 +; sta Enemy_BoundBoxCtrl+5 +; +; lda #<(-3) +; sta Enemy_Y_Speed+5 ; powerup floats upward +; +; lda #Sfx_GrowPowerUp +; sta Square2SoundQueue +; ldx ObjectOffset +; rts + +.reloc +StompedBoss: + lda Player_Y_Position + clc + adc #10 + cmp Enemy_Y_Position,x + bcc @valid_stomp +; jmp StompInjury ; player below boss -> normal stomp injury to PLAYER + jmp DamagePlayer + @valid_stomp: + lda #$fd + sta Player_Y_Speed ; bounce player upward + lda boss_state + bne InjuredBoss ; if not in state 0, just take damage + lda #0 + sta boss_state_timer ; if in idle state, also reset timer + +InjuredBoss: + lda boss_invincibility_timer + bne @done ; already invincible, no damage + inc boss_screech ; track hit count + dec Enemy_HP,x ; reduce HP + lda #80 + sta boss_invincibility_timer ; 80 frames of invincibility + @done: + rts +.reloc +DamagePlayer: + lda $a8,x + ora #$10 ; set flag indicating link was hit by this enemy? + sta $a8,x + rts +.reloc +.import ImposeGravitySprObj +MoveD_EnemyVertically: + ldy #$3d ;set quick movement amount downwards +SetHiMax: lda #$03 ;set maximum speed in A +SetXMoveAmt: sty R0 ;set movement amount here + inx ;increment X for enemy offset + jsr ImposeGravitySprObj ;do a sub to move enemy object downwards + ldx ObjectOffset ;get enemy object buffer offset and leave + rts + +;.reloc +;MoveEnemyHorizontally: +; inx ;increment offset for enemy offset +; jsr MoveObjectHorizontally ;position object horizontally according to +; ldx ObjectOffset ;counters, return with saved value in A, +; rts ;put enemy offset back in X and leave diff --git a/RandomizerCore/Asm/z2mario/boss_reference.md b/RandomizerCore/Asm/z2mario/boss_reference.md new file mode 100644 index 000000000..0c1d5d965 --- /dev/null +++ b/RandomizerCore/Asm/z2mario/boss_reference.md @@ -0,0 +1,852 @@ +# Shadow Mario Boss - Porting Reference + +A "shadow Mario" boss that replaces the original Bowser fight. The boss walks, runs, skids, jumps, shoots fireballs, and spawns bullet bills from screen edges. It has an HP system with invincibility frames on hit, a multi-state AI, and a death sequence that spawns a powerup. + +The boss uses the SMB1 enemy object system (slot 0, indexed by `ObjectOffset`/X register) and is drawn as a 4-row, 8-tile (16x32 pixel) sprite using the player's CHR tiles. + +--- + +## 1. Custom RAM Variables + +These must be allocated in your target project. Originally located at `$7e12`-`$7e1c` + `$7e75`. + +| Variable | Original Addr | Description | +|---|---|---| +| `boss_animation` | `$7e12` | Current animation frame index (see constants below) | +| `boss_invincibility_timer` | `$7e13` | Countdown after being hit; boss blinks and is immune | +| `boss_screech` | `$7e14` | Incremented on each hit (tracks total damage taken) | +| `boss_state` | `$7e15` | AI state: 0=side/idle, 1=running, 2=middle | +| `boss_state_timer` | `$7e16` | Countdown for current state duration | +| `boss_walking_anim` | `$7e17` | Walking animation sub-counter (cycles 0-1-2) | +| `boss_bridge` | `$7e18` | Flag: jump while running (set when transitioning) | +| `boss_shootanim` | `$7e1a` | Shoot animation timer (decrements each draw frame) | +| `boss_substate` | `$7e1b` | Sub-state for middle action (0=moving to center, 1=acting) | +| `boss_counter` | `$7e1c` | Death countdown timer | +| `boss_freeze` | `$7e75` | Freeze timer (boss pauses, e.g. after certain events) | +| `bbSpeed` | `$7e00` | Bullet bill horizontal speed (per enemy slot, array) | + +Total custom RAM: ~12 bytes contiguous + 2 separate bytes. + +--- + +## 2. SMB1 Engine RAM Variables Used + +These are standard SMB1 object system variables. Your target project must provide equivalents. + +### Enemy Object Arrays (indexed by X or slot number) + +| Variable | Addr | Description | +|---|---|---| +| `Enemy_ID` | `$16` | Enemy type identifier | +| `Enemy_State` | `$1e` | State flags (bit 6 = falling/airborne) | +| `Enemy_Flag` | `$0f` | Nonzero = slot active | +| `Enemy_MovingDir` | `$46` | 1=right, 2=left | +| `Enemy_X_Speed` | `$58` | Horizontal speed (signed) | +| `Enemy_X_Position` | `$87` | X position (low byte) | +| `Enemy_PageLoc` | `$6e` | X position (page/high byte) | +| `Enemy_Y_Speed` | `$a0` | Vertical speed (signed) | +| `Enemy_Y_Position` | `$cf` | Y position (low byte) | +| `Enemy_Y_HighPos` | `$b6` | Y position (high byte) | +| `Enemy_X_MoveForce` | `$0401` | Horizontal sub-pixel force | +| `Enemy_Y_MoveForce` | `$0434` | Vertical sub-pixel force | +| `Enemy_BoundBoxCtrl` | `$049a` | Bounding box size selector | +| `Enemy_CollisionBits` | `$0491` | Collision result bits | +| `Enemy_SprAttrib` | `$03c5` | Sprite attribute byte | +| `Enemy_SprDataOffset` | `$06e5` | Offset into OAM sprite data | +| `Enemy_Rel_XPos` | `$03ae` | Screen-relative X (set by `RelativeEnemyPosition`) | +| `Enemy_Rel_YPos` | `$03b9` | Screen-relative Y | + +### Player Variables + +| Variable | Addr | Description | +|---|---|---| +| `Player_X_Position` | `$86` | Player X position | +| `Player_Y_Position` | `$ce` | Player Y position | +| `Player_Y_Speed` | `$9f` | Player vertical speed | +| `PlayerStatus` | `$0756` | 0=small, 1=big, 2=fire | +| `PlayerFacingDir` | `$33` | Player facing direction | + +### Game State + +| Variable | Addr | Description | +|---|---|---| +| `ObjectOffset` | `$08` | Current enemy slot being processed (X reg) | +| `FrameCounter` | `$09` | Global frame counter | +| `TimerControl` | `$0747` | Nonzero = freeze all movement | +| `BowserHitPoints` | `$0483` | Boss HP (init to 10; 0 = dying, negative = dead) | +| `EnemyFrenzyBuffer` | `$06cb` | Cleared by boss each frame to prevent frenzy spawns | +| `PseudoRandomBitReg` | `$07a7` | PRNG value | +| `PowerUpType` | `$39` | Type of powerup to spawn | + +### Sprite OAM + +| Variable | Addr | Description | +|---|---|---| +| `Sprite_Y_Position` | `$0200` | OAM Y position (indexed by sprite data offset) | +| `Sprite_Tilenumber` | `$0201` | OAM tile number | +| `Sprite_Attributes` | `$0202` | OAM attributes (palette, flip, priority) | +| `Sprite_X_Position` | `$0203` | OAM X position | + +### Sound Queues + +| Variable | Addr | Description | +|---|---|---| +| `Square1SoundQueue` | `$ff` | Sound effect queue channel 1 | +| `Square2SoundQueue` | `$fe` | Sound effect queue channel 2 | +| `AreaMusicQueue` | `$fb` | Music queue | + +### Scratch RAM + +The boss routines use zero-page scratch: `$00`-`$05`, `$eb`. These are assumed temporary and clobbered freely. + +### Other + +| Variable | Addr | Description | +|---|---|---| +| `VerticalFlipFlag` | `$0109` | Used by `DrawSpriteObject` | +| `Alt_SprDataOffset` | `$06ec` | Alternate sprite data offset (for multi-row draws) | +| `pal_type` | `$7e78` | Custom: palette type (set to 3 on boss death) | + +--- + +## 3. Constants + +```asm +; Animation frame indices (into BossGraphics table) +SKIDDING = 3 +JUMPING = 4 +SHOOT = 5*8 ; = 40, byte offset for shoot frame +STANDING = 6 + +; Sound effects +Sfx_Fireball = %00100000 +Sfx_BigJump = %00000001 +Sfx_Blast = %00001000 +Sfx_GrowPowerUp = %00000010 +Silence = %10000000 + +; Music +mus_bossdie = 8 + +; Enemy IDs +BulletBill_FrenzyVar = $08 +BowserFlame = $15 +Bowser = $2d +PowerUpObject = $2e +``` + +--- + +## 4. Graphics Table + +The boss is drawn using player-like CHR tiles. Each animation frame is 8 bytes (4 rows of 2 tile IDs). + +```asm +BossGraphics: +; row0 row1 row2 row3 (each row = 2 tiles: left, right) + .db $00, $01, $02, $03, $04, $05, $06, $07 ; frame 0: walking 1 + .db $08, $09, $0a, $0b, $0c, $0d, $0e, $0f ; frame 1: walking 2 + .db $10, $11, $12, $13, $14, $15, $16, $17 ; frame 2: walking 3 + .db $18, $19, $1a, $1b, $1c, $1d, $1e, $1f ; frame 3: skidding + .db $20, $21, $22, $23, $24, $25, $26, $27 ; frame 4: jumping + .db $08, $09, $28, $29, $2a, $2b, $0e, $0f ; frame 5: fireball throwing (top half reuses walk2) + .db $00, $01, $4c, $4d, $4a, $51, $4b, $52 ; frame 6: standing +``` + +--- + +## 5. Init Routine + +Called when the boss enemy object is first created. Sets HP, direction, and timers. + +```asm +InitBoss: + lda Enemy_X_Position,x + sta BowserOrigXPos ;store original horizontal position + lda #$df + sta BowserFireBreathTimer ;store timer value + lda #2 + sta Enemy_MovingDir,x ;face left + lda #$20 + sta BowserFeetCounter ;set timer + sta EnemyFrameTimer,x + lda #10 + sta BowserHitPoints ;10 hit points + lsr + sta BowserMovementSpeed ;default movement speed = 5 + rts +``` + +> **Note:** `BowserOrigXPos`, `BowserFeetCounter`, `BowserMovementSpeed`, `BowserFireBreathTimer`, and `EnemyFrameTimer` are used by the original Bowser init but are NOT used by the shadow Mario `RunBoss` code. You likely only need to initialize `BowserHitPoints`, `Enemy_MovingDir`, and clear the `boss_*` RAM block to zero. + +### Minimal Init for Porting + +```asm +InitShadowBoss: + ; Clear all boss RAM + lda #0 + ldx #BOSS_RAM_SIZE + @loop: + sta boss_ram_start,x + dex + bpl @loop + sta boss_freeze + + ; Set HP + lda #10 + sta BowserHitPoints + + ; Face left + ldx ObjectOffset + lda #2 + sta Enemy_MovingDir,x + rts +``` + +--- + +## 6. Update Routine (Main Entry Point) + +Called once per frame while the boss is alive. This is the top-level boss tick. + +```asm +RunBoss: + lda boss_freeze + beq @notfreezed + dec boss_freeze + lda #STANDING + sta boss_animation + ; Draw every other frame (flicker) + lda FrameCounter + lsr + bcs @nodraw2 + jsr RelativeEnemyPosition + jsr BossGraphicsHandler + @nodraw2: + ldx ObjectOffset + rts + + @notfreezed: + lda BowserHitPoints + bmi AnimateKilledBoss ; HP < 0: boss is fully dead + jeq KillBoss ; HP == 0: playing death countdown + + lda #80 + sta boss_counter ; reset death countdown each frame while alive + lda #0 + sta EnemyFrenzyBuffer ; suppress enemy frenzy spawns + + ; Draw (skip every other frame if invincible for blink effect) + lda boss_invincibility_timer + beq @draw + lda FrameCounter + lsr + bcs @nodraw + @draw: + jsr RelativeEnemyPosition + jsr BossGraphicsHandler + @nodraw: + ldx ObjectOffset + + lda TimerControl + bne @nograv ; skip movement if timer frozen + + jsr EnemyToBGCollisionDet ; background collision + jsr HandleBossMovement ; AI state machine + + lda Enemy_State,x + and #%01000000 ; check airborne flag + beq @nograv + jsr MoveD_EnemyVertically ; apply gravity + lda #JUMPING + sta boss_animation + @nograv: + + ; Collision detection (only when not invincible) + lda boss_invincibility_timer + bne @decrement_timer + lda #$0a + sta Enemy_BoundBoxCtrl,x ; set bounding box size + jsr GetEnemyOffscreenBits + jsr GetEnemyBoundBox + jmp PlayerEnemyCollision ; check player<->boss collision + + @decrement_timer: + dec boss_invincibility_timer + rts +``` + +--- + +## 7. AI State Machine + +`HandleBossMovement` dispatches based on `boss_state`: + +```asm +HandleBossMovement: + lda boss_state + jsr JumpEngine + .dw MovBossSide ; state 0 + .dw MovBossRunning ; state 1 + .dw MovBossMiddle ; state 2 +``` + +### State 0: Side/Idle (`MovBossSide`) + +Boss stands in place, randomly shoots fireballs and jumps. After `boss_state_timer` expires, transitions to either state 1 (running) or state 2 (middle) based on random + player position. + +```asm +MovBossSide: + lda #STANDING + sta boss_animation + + ; Random fireball: every 8th frame, 1/4 chance + lda FrameCounter + and #%00000111 + bne @no_fireball + lda PseudoRandomBitReg + and #%00000011 + bne @no_fireball + jsr BossSpawnFireball + @no_fireball: + + ; Random jump: every 8th frame (offset 4), 1/2 chance + lda Enemy_State,x + and #%01000000 + bne @no_jump + lda FrameCounter + and #%00000111 + cmp #4 + bne @no_jump + lda PseudoRandomBitReg + lsr + bcc @no_jump + lda #-5 + sta Enemy_Y_Speed,x + jsr BossJump + @no_jump: + + ; State transition when timer expires + lda boss_state_timer + bne @keep_state + + lda #0 + sta boss_substate + lda FrameCounter + clc + adc Player_X_Position + lsr + lsr + bcs @middle + lda #2 ; -> state 2 (middle) + .db $2c ; skip next instruction (BIT abs trick) + @middle: + lda #1 ; -> state 1 (running) + sta boss_state + inc boss_bridge + + @keep_state: + dec boss_state_timer + rts +``` + +### State 1: Running (`MovBossRunning`) + +Boss runs toward the opposite side, accelerating then decelerating/skidding. Optionally jumps if `boss_bridge` is set. + +```asm +MovBossRunning: + jsr BossSetWalkingAnim + + lda Enemy_MovingDir,x + cmp #$01 + beq MovBossRight + + ; Moving left + lda Enemy_X_Position,x + cmp #11*16 + bcs @StillAccer + cmp #4*16 + bcs MovStillMoving + @Deaccelerating: + lda #SKIDDING + sta boss_animation + inc Enemy_X_Speed,x + inc Enemy_X_Speed,x + lda Enemy_X_Speed,x + bmi MovStillMoving + jmp MovChangeState + @StillAccer: + dec Enemy_X_Speed,x + dec Enemy_X_Speed,x + +MovStillMoving: + lda boss_bridge + beq @no_jump + dec boss_bridge + lda Enemy_State,x + and #%01000000 + bne @no_jump + lda #-4 + sta Enemy_Y_Speed,x + jsr BossJump + @no_jump: + jmp MoveEnemyHorizontally + +MovBossRight: + lda Enemy_X_Position,x + cmp #4*16 + bcc @StillAccer + cmp #11*16 + bcc MovStillMoving + @Deaccelerating: + lda #SKIDDING + sta boss_animation + dec Enemy_X_Speed,x + dec Enemy_X_Speed,x + lda Enemy_X_Speed,x + bpl MovStillMoving + jmp MovChangeState + @StillAccer: + inc Enemy_X_Speed,x + inc Enemy_X_Speed,x + jmp MoveEnemyHorizontally + +MovChangeState: + lda PseudoRandomBitReg + and #$f0 + sta boss_state_timer ; random duration for next idle + lda #STANDING + sta boss_animation + lda #0 + sta boss_state ; -> state 0 (side/idle) + lda Enemy_MovingDir,x + eor #%00000011 ; flip direction + sta Enemy_MovingDir,x + rts +``` + +### State 2: Middle (`MovBossMiddle`) + +Boss walks to center of arena, then stands and shoots bullet bills from screen edges. After all enemies are cleared, spawns a powerup if player needs one, then transitions to state 1 (running). + +```asm +MovBossMiddle: + lda boss_substate + beq MovGoToMiddle ; substate 0: walk to center + ; substate 1: middle action (shoot bullets, wait for enemies to die) + + lda #STANDING + sta boss_animation + lda boss_state_timer + bne @not_ready + + ; Timer expired: check if all enemy slots 1-4 are clear + ldy #1 + @loop: + lda Enemy_Flag,y + bne @notyet ; slot still occupied, wait + iny + cpy #5 + bcc @loop + + ; All clear: maybe spawn powerup + lda PlayerStatus + cmp #2 + bcs @no_need_for_p ; player already has fire power + sta PowerUpType + lda #PowerUpObject + cmp Enemy_ID+5 + beq @no_need_for_p ; powerup already exists + ; ... (spawns powerup in slot 5) ... + @no_need_for_p: + lda #0 + sta boss_bridge + lda #1 + sta boss_state ; -> state 1 (running) + ; Set speed based on facing direction + lda Enemy_MovingDir,x + lsr + bcs @right + lda #-$20 + .db $2c + @right: + lda #$20 + sta Enemy_X_Speed,x + @notyet: + rts + + @not_ready: + dec boss_state_timer + ; Every 16 frames, spawn a bullet bill from a random Y position + lda FrameCounter + and #%00001111 + bne @done + ; ... (random Y calculation, alternates left/right side) ... + jsr BossSpawnSideBullet + @done: + rts + +MovGoToMiddle: + ; Walks toward X=$40 (right-facing) or X=$B0 (left-facing) + ; Jumps onto the center platform when reaching the target + ; Sets boss_substate=1 and boss_state_timer when arrived + ; ... (see full source for details) ... +``` + +### Helpers + +```asm +BossSetWalkingAnim: + lda FrameCounter + and #%00000011 + cmp #%00000011 + bne @init_anim + inc boss_walking_anim + lda boss_walking_anim + cmp #$03 + bne @init_anim + lda #0 + sta boss_walking_anim + @init_anim: + lda boss_walking_anim + sta boss_animation + rts + +BossJump: + lda #Sfx_BigJump + sta Square1SoundQueue + lda Enemy_State,x + ora #%01000000 ; set airborne flag + sta Enemy_State,x + rts +``` + +--- + +## 8. Draw Routine + +Draws the boss as a 16x32 pixel sprite (4 rows of 2 tiles). Uses the SMB1 `DrawSpriteObject` routine. + +```asm +BossGraphicsHandler: + lda Enemy_Y_Position,x + sta $02 ; Y position + lda Enemy_Rel_XPos + sta $05 ; screen-relative X + sta VerticalFlipFlag + lda #0 + ldy boss_animation + cpy #SKIDDING + bne + + lda #%00000011 ; flip both axes when skidding + + eor Enemy_MovingDir,x + sta $03 ; horizontal flip control + lda #1 + sta $04 ; sprite palette + + ldy Enemy_SprDataOffset,x + lda boss_shootanim + bne @shootanim + + ; Normal draw: look up animation frame in BossGraphics + lda boss_animation + asl + asl + asl ; * 8 = byte offset + tax + jsr DrawBossRow ; row 0 (head) + jsr DrawBossRow ; row 1 (torso) + ldy Alt_SprDataOffset+1 + jsr DrawBossRow ; row 2 (legs top) + @drawrest: + ldy $06eb + jmp DrawBossRow ; row 3 (legs bottom) + + @shootanim: + ; Shoot: draw shoot frame for top 2 rows, normal for bottom 2 + dec boss_shootanim + ldx #SHOOT ; = 40, offset to shoot frame + jsr DrawBossRow + jsr DrawBossRow + ldy Alt_SprDataOffset+1 + jsr DrawBossRow + lda boss_animation + asl + asl + asl + clc + adc #6 ; skip to bottom 2 tiles of normal frame + tax + jmp @drawrest + +DrawBossRow: + lda BossGraphics,x + sta $00 ; left tile + lda BossGraphics+1,x + sta $01 ; right tile + jmp DrawSpriteObject ; engine routine: writes 2 sprites to OAM +``` + +--- + +## 9. Projectile Spawning + +### Spawn Fireball + +Fires a `BowserFlame` projectile in the boss's facing direction. + +```asm +BossSpawnFireball: + jsr FindID ; find empty enemy slot -> Y + bmi @done ; no slot available + lda #Sfx_Fireball + sta Square1SoundQueue + lda #4 + sta boss_shootanim ; trigger shoot animation + lda Enemy_Y_Position,x + clc + adc #4 + sta $00 ; Y pos + lda Enemy_X_Position,x + sta $01 ; X pos + lda #0 + sta $02 ; page + lda #8 + sta $03 ; bounding box size + lda Enemy_MovingDir,x + cmp #$01 + bne @no + pha + lda $01 + clc + adc #8 ; offset X if facing right + sta $01 + pla + @no: + sta Enemy_MovingDir,y + lda #BowserFlame + jsr BossSpawnEnemy + ldx ObjectOffset + @done: + rts +``` + +### Spawn Side Bullet + +Spawns a `BulletBill_FrenzyVar` from the left or right edge of the screen at a given Y position. + +```asm +; A = direction (1=left edge, 2=right edge) +; $00 = Y position +BossSpawnSideBullet: + sta $02 + jsr FindID + bmi @done + lda $02 + pha + lda #Sfx_Blast + sta Square2SoundQueue + + lda $02 + lsr + bcs @rightdir + + lda #$f0 + sta $01 ; X = left edge ($F0) + lda #-60 + sta bbSpeed,y ; speed = -60 (move right... from left) + jmp @rest + + @rightdir: + lda #0 + sta $01 ; X = right edge ($00) + lda #60 + sta bbSpeed,y ; speed = 60 (move left... from right) + + @rest: + lda #8 + sta $03 ; bounding box + lda #0 + sta $02 ; page + lda #BulletBill_FrenzyVar + sty $eb + jsr BossSpawnEnemy + ldy $eb + pla + sta Enemy_MovingDir,y + ldx ObjectOffset + @done: + rts +``` + +### Helpers + +```asm +; Spawn enemy in slot Y with parameters in $00-$03 +; A = enemy ID +BossSpawnEnemy: + sta Enemy_ID,y + lda $00 + sta Enemy_Y_Position,y + lda $01 + sta Enemy_X_Position,y + lda $02 + sta Enemy_PageLoc,y + lda $03 + sta Enemy_BoundBoxCtrl,y + lda #$01 + sta Enemy_Y_HighPos,y + sta Enemy_Flag,y + lsr + sta Enemy_X_MoveForce,y + sta Enemy_State,y + ldx $04 ; slot index + jmp CheckpointEnemyID ; engine: initialize enemy by ID + +; Find first empty enemy slot (1-5), return in Y. N flag set if none found. +FindID: + ldy #5 + @loop: + lda Enemy_Flag,y + beq @foundSlot + dey + bpl @loop + @foundSlot: + sty $04 + rts +``` + +--- + +## 10. Death Sequence + +### KillBoss (HP == 0 countdown) + +```asm +KillBoss: + lda boss_counter + bmi + ; counter expired -> signal fully dead + lda #Silence + sta AreaMusicQueue + dec boss_counter + + jsr EnemyToBGCollisionDet + lda #STANDING + sta boss_animation + lda Enemy_State,x + and #%01000000 + beq @nograv + jsr MoveD_EnemyVertically + lda #JUMPING + sta boss_animation + @nograv: + ; Draw every other frame (flash effect) + lda FrameCounter + lsr + bcs @nodraw + jsr RelativeEnemyPosition + jsr BossGraphicsHandler + @nodraw: + ldx ObjectOffset + rts + + + ; Counter expired: signal death complete + dec BowserHitPoints ; HP goes to $FF (negative) + lda #mus_bossdie + sta AreaMusicQueue + rts +``` + +### AnimateKilledBoss (HP < 0: spawn powerup and erase) + +```asm +AnimateKilledBoss: + jsr EraseEnemyObject + + lda #3 + sta pal_type ; custom: set palette type for post-boss + lda #2 + sta PowerUpType + lda #PowerUpObject + sta Enemy_ID+5 + lda #0 + sta Enemy_PageLoc+5 + sta Enemy_X_Speed+5 + lda Enemy_X_Position,x + sta Enemy_X_Position+5 ; spawn at boss's position + lda #$01 + sta Enemy_Y_HighPos+5 + sta Enemy_Flag+5 + lda Enemy_Y_Position,x + sta Enemy_Y_Position+5 + lda #$80 + sta Enemy_State+5 + lda #$03 + sta Enemy_BoundBoxCtrl+5 + + lda #-3 + sta Enemy_Y_Speed+5 ; powerup floats upward + + lda #Sfx_GrowPowerUp + sta Square2SoundQueue + ldx ObjectOffset + rts +``` + +--- + +## 11. Damage Handling + +Called when player stomps or fireballs hit the boss. Located separately from the main boss code. + +```asm +StompedBoss: + lda Player_Y_Position + clc + adc #10 + cmp Enemy_Y_Position,x + bcc + + jmp StompInjury ; player below boss -> normal stomp injury to PLAYER + + lda #$fd + sta Player_Y_Speed ; bounce player upward + lda boss_state + bne InjuredBoss ; if not in state 0, just take damage + lda #0 + sta boss_state_timer ; if in idle state, also reset timer + +InjuredBoss: + lda boss_invincibility_timer + bne @done ; already invincible, no damage + inc boss_screech ; track hit count + dec BowserHitPoints ; reduce HP + lda #80 + sta boss_invincibility_timer ; 80 frames of invincibility + @done: + rts +``` + +--- + +## 12. External Dependencies + +Your target project must provide these routines: + +| Routine | Description | +|---|---| +| `EnemyToBGCollisionDet` | Check enemy object vs background tile collision. Updates `Enemy_State` flags. | +| `MoveD_EnemyVertically` | Apply gravity/vertical speed to enemy. Uses `Enemy_Y_Speed`, `Enemy_Y_Position`. | +| `MoveEnemyHorizontally` | Apply horizontal speed to enemy. Uses `Enemy_X_Speed`, `Enemy_X_Position`. | +| `RelativeEnemyPosition` | Calculate screen-relative X/Y from absolute position. Stores to `Enemy_Rel_XPos`/`Enemy_Rel_YPos`. | +| `GetEnemyOffscreenBits` | Determine if enemy is off any screen edge. | +| `GetEnemyBoundBox` | Set up bounding box coordinates from `Enemy_BoundBoxCtrl`. | +| `PlayerEnemyCollision` | Detect and handle player vs enemy collision (calls `StompedBoss` for this enemy). | +| `DrawSpriteObject` | Draw a row of 2 sprites to OAM. Reads tile IDs from `$00`/`$01`, position from `$02`/`$05`, attributes from `$03`/`$04`. Advances Y by 8 for next row. | +| `EraseEnemyObject` | Clear enemy from active slot (zero out `Enemy_Flag`, etc). | +| `JumpEngine` | Indirect jump dispatch: reads A as index, jumps to address from following `.dw` table. | +| `MoveFireballVertically` | Apply gravity to fireball projectiles (used by `RunFireball`, not included here). | +| `OffscreenBoundsCheck` | Despawn enemy if too far offscreen (used by `RunFireball`, not included here). | +| `CheckpointEnemyID` | Initialize a newly spawned enemy by its ID. X = slot index. | +| `StompInjury` | Standard SMB1 stomp injury to player (when stomp fails). | +| `DrawFirebar` | Draw a single firebar/fireball sprite (used by fireball draw, not included here). | +| `ProcFireball` / `RunFireball` | Fireball projectile update logic (not included; you need your own projectile system). | diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 1fc04cfc1..6025a9099 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -18,7 +18,7 @@ FREE "PRG0" [$9815, $9925) ; .org (($10ea - $10) .mod $4000) + $8000 ; .byte $17 -.segment "PRG0", "PRG7" +.segment "PRG7" SpellCastingRoutine = $8DC3 ; Link Main .org $D3EC ; patches the main sideview routine right before checking marios code jsr SwapMarioCHRBanks @@ -39,6 +39,12 @@ BankSwitchMarioCHR: ; nop lda PlayerChrBank sta SpChrBank0Reg + 0 + lda boss_animation ; if we are running the custom shadow mario boss + ; also switch out the other sprite bank + beq @noboss + lda boss_ChrBank + sta SpChrBank0Reg + 3 + @noboss: lda CurrentCHRBank ; we need to write a BG bank register due to MMC5 jank sta BgChrBank0Reg lda #0 @@ -46,6 +52,7 @@ BankSwitchMarioCHR: + rts +.segment "PRG0", "PRG7" ; patch the link draw on lives screen to set the y hi position properly .org $c3fb sta Player_X_Position ; Use the regular X position instead of the "screen" version @@ -80,6 +87,7 @@ PatchLinkLivesScreenDraw: .reloc AlsoResetPlayerSizeOnContinue: sta PlayerSize + sta boss_animation ; and clear the dark link flag sta $075c ; original code rts @@ -104,10 +112,16 @@ SetHurtMetasprite: PatchLinkDeathSprite: lda #METASPRITE_SMALL_MARIO_DEATH sta ObjectMetasprite + lda #0 + sta boss_animation lda #$0b ; player death subroutine sta GameEngineSubroutine jsr PlayerGfxHandler inc ReloadCHRBank + ; Swap the vanilla CHR bank back in to reset the dark link graphics + lda $076E ; Vanilla CHR Bank we are using +.import SwapCHR + jsr SwapCHR jmp BankSwitchMarioCHR .org $8fe3 @@ -196,6 +210,15 @@ PatchLinkDrawRoutine: inc ReloadCHRBank + @skipplayer: + ; Check if we are rendering the Mario Boss + ldx boss_animation + beq @noboss + ldy #1 + lda #2 + sta SprObject_SprAttrib,y + jsr DrawMetasprite + ; TODO draw boss spawned objects here +@noboss: ; While we are here, lets draw the fireball/hammer sprites too ldy #ProjectileOffset sty $02 diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index a818a93a7..07aa6beb6 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -1657,7 +1657,7 @@ ExPlayerAttr: ; .byte $22, $30, $27, $19 ;luigi's colors ; .byte $22, $37, $27, $16 ;fiery (used by both) - +.export ImposeGravitySprObj ImposeGravitySprObj: sta R2 ;set maximum speed here lda #$00 ;set value to move downwards diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index 8f68e4ee9..fb08113d9 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -428,34 +428,58 @@ SetYOffset: sta Atr txa + pha + tya pha jsr MetaspriteRenderLoop pla + tay + pla cmp #LAST_MARIO_METASPRITE bcs @exit + ; Check if this is the boss + cpy #0 + beq @isplayer + ; Rendering dark mario so use the dark mario pattern table + ldx CurrentOAMOffset + lda #%11000000 + ora Sprite_Tilenumber - 16,x + sta Sprite_Tilenumber - 16,x + lda #%11000000 + ora Sprite_Tilenumber - 12,x + sta Sprite_Tilenumber - 12,x + lda #%11000000 + ora Sprite_Tilenumber - 8,x + sta Sprite_Tilenumber - 8,x + lda #%11000000 + ora Sprite_Tilenumber - 4,x + sta Sprite_Tilenumber - 4,x + rts + @isplayer: ; We are rendering a mario sprite, so check if we are stuck in the mud lda $0752 and #$20 beq @exit ; stuck in the mud, so update bottom two sprite ; x = next oam sprite id, so we can offset by 8 to hit bottom two sprites + ldx CurrentOAMOffset ; if we are small then its the most recent two sprites. if we are big then its 4 sprites back lda PlayerSize beq + ; If we are large - lda $0202 - 8, x + lda Sprite_Attributes - 8, x ora #$20 - sta $0202 - 8, x - lda $0202 - 4, x + sta Sprite_Attributes - 8, x + lda Sprite_Attributes - 4, x ora #$20 - sta $0202 - 4, x + sta Sprite_Attributes - 4, x rts + - lda $0202 - 16, x + lda Sprite_Attributes - 16, x ora #$20 - sta $0202 - 16, x - lda $0202 - 12, x + sta Sprite_Attributes - 16, x + lda Sprite_Attributes - 12, x ora #$20 - sta $0202 - 12, x + sta Sprite_Attributes - 12, x @exit: rts diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index d26fae9ee..99bcaddda 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -128,8 +128,10 @@ MulHiReg = $5206 NewSideviewInit = $8cec ClearNametables = $d266 JumpEngine = $d385 +EnemyFacingDirection = $DC91 LoadPlayerShieldCollisionBox = $e9d8 ObjectCollisionCheck = $e9f9 +MoveEnemyHorizontally = $deb8 ; These are new variables that use free memory Z2Noise_SfxLenCounter = $07f5 @@ -147,6 +149,18 @@ NoiseSoundQueue = $f0 Square2SoundQueue = $f1 Square1SoundQueue = $f2 +ProjectileYPosition = $30 +ProjectileXVelocity = $77 +ProjectileType = $87 +EnemyType = $a1 +Enemy_Flag = ProjectileType +LinkXPosition = $4d +EnemyXPositionLo = $4e +EnemyXPositionHi = $3c +Enemy_HP = $c2 +RNG = $051b +ProjectileEnemyData = $07c0 +EnemyYVelocity = $057e ;; NEW VARIABLE ALLOCATIONS ; Stack RAM @@ -254,7 +268,7 @@ RESV StarInvincibleTimer ALL_TIMER_COUNT = StarInvincibleTimer - Timers RESV SpriteShuffleOffset -RESV CurrentCHRBank +;RESV CurrentCHRBank RESV PlayerChrBank RESV ReloadCHRBank RESV ObjectVerticalFlip @@ -275,6 +289,23 @@ RESV Squ1_SfxLenCounter RESV Squ2_SfxLenCounter RESV Noise_SfxLenCounter +; Shadow Mario Boss RAM +CUSTOM_BOSS_START = RES_OFFSET +RESV boss_animation +RESV boss_invincibility_timer +RESV boss_screech +RESV boss_state +RESV boss_state_timer +RESV boss_walking_anim +RESV boss_bridge +RESV boss_shootanim +RESV boss_substate +RESV boss_counter +RESV boss_freeze +RESV boss_ChrBank +BOSS_RAM_SIZE = RES_OFFSET - CUSTOM_BOSS_START - 1 +;RESV BowserHitPoints + ; MARIO SPECIFIC RAM LOCATIONS ; These are values that are hardcoded to match where Zelda 2 places them Player_X_Speed = $70 @@ -304,13 +335,26 @@ Fireball_Y_Speed = SprObject_Y_Speed + FireballOffset RESV FireballBouncingFlag, 2 RESV Fireball_State, 2 +EnemyOffset = 1 ; Enemy slot 0 = offset 1 from player +Enemy_X_Position = SprObject_X_Position + EnemyOffset +Enemy_Y_Position = SprObject_Y_Position + EnemyOffset +Enemy_X_Speed = SprObject_X_Speed + EnemyOffset +Enemy_PageLoc = SprObject_PageLoc + EnemyOffset +Enemy_Y_HighPos = SprObject_Y_HighPos + EnemyOffset +Enemy_MovingDir = Player_MovingDir + EnemyOffset +Enemy_State = $A8 + + ; For reasons I cannot explain, player_x_moveforce is not the same as sprobject_x_moveforce SprObject_X_MoveForce = $03d6 ; Player_X_MoveForce Player_Y_MoveForce = $03e6 SprObject_Y_MoveForce = Player_Y_MoveForce +Enemy_X_MoveForce = SprObject_X_MoveForce + EnemyOffset +Enemy_Y_MoveForce = SprObject_Y_MoveForce + EnemyOffset Player_Y_Speed = $057D SprObject_Y_Speed = Player_Y_Speed +Enemy_Y_Speed = SprObject_Y_Speed + EnemyOffset FrameCounter = $12 ; CrouchingFlag = $17 @@ -401,7 +445,7 @@ DeathMusic = %00000001 .endif ; ENABLE_Z2_MARIO -.assert RES_BASE + RES_OFFSET < $01ad +.assert RES_BASE + RES_OFFSET < $01bd RelocatedRAMBase = $041A @@ -521,6 +565,18 @@ Fireball_SprAttrib = SprObject_SprAttrib + FireballOffset RESV Player_YMoveForceFractional, 16 +; Enemy-specific arrays (6 slots each: boss=0, enemies 1-5) +; RESV Enemy_ID, 6 +; RESV Enemy_State, 6 +; RESV Enemy_Flag, 6 +; RESV Enemy_BoundBoxCtrl, 6 +; RESV Enemy_CollisionBits, 6 +; RESV Enemy_SprDataOffset, 6 +; RESV Enemy_Rel_XPos, 6 +; RESV Enemy_Rel_YPos, 6 +; RESV bbSpeed, 6 ; Bullet bill speed per slot +; Enemy_SprAttrib = SprObject_SprAttrib + EnemyOffset + ClearSRAMEnd = RES_OFFSET diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 22bea32e6..1daf7b406 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3805,7 +3805,7 @@ private void ChangeMapperToMMC5(Assembler asm, bool preventFlash, bool enableZ2F private void MarioModeActivate(Assembler asm) { // "metasprite_engine.s" is included in metasprite.s - string[] modules = ["integration.s", "map.s", "mario.s", "metasprite.s", "sfx.s"]; + string[] modules = ["boss.s", "integration.s", "map.s", "mario.s", "metasprite.s", "sfx.s"]; foreach (var mod in modules) { var a = asm.Module(); diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index ca6023603..fb8435956 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -1914,8 +1914,6 @@ public void FixMinibossGlitchyAppearance(Assembler asm) .include "z2r.inc" .import SwapCHR -EnemyFacingDirection = $DC91 - .segment "PRG7" ; Patch the start of the sideview initialization to check if the enemy is loaded in the first screen diff --git a/RandomizerCore/RandomizerCore.csproj b/RandomizerCore/RandomizerCore.csproj index 728bf979f..b3268e3fe 100644 --- a/RandomizerCore/RandomizerCore.csproj +++ b/RandomizerCore/RandomizerCore.csproj @@ -87,5 +87,7 @@ + + From 5da849988df946780171a7eefd4c73559bc5cdf1 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 28 Apr 2026 01:22:17 -0400 Subject: [PATCH 017/128] Bring it back to .net 10 --- CommandLine/DefaultPlayerOptions.json | 4 ++-- .../PublishProfiles/FolderProfile.pubxml | 2 +- CommandLine/Properties/launchSettings.json | 2 +- .../CrossPlatformUI.Browser.csproj | 3 ++- .../PublishProfiles/FolderProfile.pubxml | 2 +- .../CrossPlatformUI.Desktop.csproj | 2 +- CrossPlatformUI/CrossPlatformUI.csproj | 2 +- Directory.Build.props | 2 +- RandomizerCore/Asm/MMC5.s | 13 +++++------- RandomizerCore/Asm/z2mario/integration.s | 4 ++++ RandomizerCore/Hyrule.cs | 20 +++++++++++++++++++ Statistics/Statistics.csproj | 2 +- Tests/Tests.csproj | 2 +- 13 files changed, 41 insertions(+), 19 deletions(-) diff --git a/CommandLine/DefaultPlayerOptions.json b/CommandLine/DefaultPlayerOptions.json index 4ca1038b6..a3f33c406 100644 --- a/CommandLine/DefaultPlayerOptions.json +++ b/CommandLine/DefaultPlayerOptions.json @@ -1,7 +1,7 @@ { "DisableMusic": false, - "RandomizeMusic": false, - "MixCustomAndOriginalMusic": false, + "RandomizeMusic": true, + "MixCustomAndOriginalMusic": false, "IncludeDiverseMusic": false, "DisableUnsafeMusic": true, "FastSpellCasting": true, diff --git a/CommandLine/Properties/PublishProfiles/FolderProfile.pubxml b/CommandLine/Properties/PublishProfiles/FolderProfile.pubxml index 3627f0c91..3d615926b 100644 --- a/CommandLine/Properties/PublishProfiles/FolderProfile.pubxml +++ b/CommandLine/Properties/PublishProfiles/FolderProfile.pubxml @@ -6,7 +6,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Release Any CPU - bin\Release\net8.0\publish\win-x64\ + bin\Release\net10.0\publish\win-x64\ FileSystem <_TargetId>Folder win-x64 diff --git a/CommandLine/Properties/launchSettings.json b/CommandLine/Properties/launchSettings.json index cb373ceee..34681a8d9 100644 --- a/CommandLine/Properties/launchSettings.json +++ b/CommandLine/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "CommandLine": { "commandName": "Project", - "commandLineArgs": "-f AAAA2dJAJBAAAAAAABALyPyk+hAAAAAAAAAAAAABAC -r ../Zelda2.nes -s 32165874 -o ..", + "commandLineArgs": "-f AAABvvyhAmAAAAAAAAAATy9Q#AAAAAAAAAAAAACRA -r ../Zelda2.nes -s 32165874 -o ..", "workingDirectory": "../build/" } } diff --git a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj index 24ae77f49..4bede0a2e 100644 --- a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj +++ b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj @@ -1,7 +1,7 @@  Exe - net8.0-browser + net10.0-browser browser-wasm wwwroot\main.js ./_framework @@ -93,6 +93,7 @@ + diff --git a/CrossPlatformUI.Browser/Properties/PublishProfiles/FolderProfile.pubxml b/CrossPlatformUI.Browser/Properties/PublishProfiles/FolderProfile.pubxml index f165eeba6..056667b1d 100644 --- a/CrossPlatformUI.Browser/Properties/PublishProfiles/FolderProfile.pubxml +++ b/CrossPlatformUI.Browser/Properties/PublishProfiles/FolderProfile.pubxml @@ -7,7 +7,7 @@ publish\ FileSystem <_TargetId>Folder - net8.0-browser + net10.0-browser browser-wasm true false diff --git a/CrossPlatformUI.Desktop/CrossPlatformUI.Desktop.csproj b/CrossPlatformUI.Desktop/CrossPlatformUI.Desktop.csproj index 60f116f2a..6065e2a2b 100644 --- a/CrossPlatformUI.Desktop/CrossPlatformUI.Desktop.csproj +++ b/CrossPlatformUI.Desktop/CrossPlatformUI.Desktop.csproj @@ -2,7 +2,7 @@ WinExe + One for Windows with net10.0-windows TFM, one for MacOS with net10.0-macos and one with net10.0 TFM for Linux.--> enable true true diff --git a/CrossPlatformUI/CrossPlatformUI.csproj b/CrossPlatformUI/CrossPlatformUI.csproj index 36406d1c1..9c261a452 100644 --- a/CrossPlatformUI/CrossPlatformUI.csproj +++ b/CrossPlatformUI/CrossPlatformUI.csproj @@ -40,7 +40,7 @@ - + all diff --git a/Directory.Build.props b/Directory.Build.props index 920ee012b..bd965de9d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - net8.0 + net10.0 enable 1.0.7 11.3.12 diff --git a/RandomizerCore/Asm/MMC5.s b/RandomizerCore/Asm/MMC5.s index afcd6665f..f0fa5721b 100644 --- a/RandomizerCore/Asm/MMC5.s +++ b/RandomizerCore/Asm/MMC5.s @@ -203,11 +203,11 @@ HandleLagFrame: @HandleAudio: ; Skip processing audio during a real lag frame since thats what ; vanilla accomplishes with a hard disabled NMI - lda SoftDisableNmi - bne @skip - jsr UpdateSound -@skip: - rts +; lda SoftDisableNmi +; bne @skip + jmp UpdateSound +;@skip: +; rts .segment "PRG7" @@ -534,9 +534,6 @@ SwapCHR: adc #1 sta SpChrBank4Reg sta BgChrBank0Reg -;.ifdef ENABLE_Z2_MARIO -; sta CurrentCHRBank ; Store a copy of the latest BG bank for use in z2mario -;.endif adc #1 sta SpChrBank5Reg sta BgChrBank1Reg diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 6025a9099..3e342580f 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -46,6 +46,10 @@ BankSwitchMarioCHR: sta SpChrBank0Reg + 3 @noboss: lda CurrentCHRBank ; we need to write a BG bank register due to MMC5 jank + asl + asl + clc + adc #4 sta BgChrBank0Reg lda #0 sta ReloadCHRBank diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 1daf7b406..e214443cc 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3953,7 +3953,27 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando { rom.ApplyIps( Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2rndft.ips")); + // really hacky workaround but i don't want to recompile z2ft + // z2ft is compiled using an old address for NmiBankShadow8 and NmiBankShadowA so rather than + // recompile that, I'm just gonna patch it here... + const byte LDA_ABS = 0xAD; + const byte STA_ABS = 0x8D; + void PatchAddress(byte opcode, int before, int after) + { + var idx = 0; + var needle = new ReadOnlySpan([opcode, (byte)before, (byte)(before >> 8)]); + while (rom.rawdata.AsSpan(idx).IndexOf(needle) is var di and >= 0) + { + rom.Put(idx + di, opcode, (byte)after, (byte)(after >> 8)); + idx += di + 1; + } + } + + PatchAddress(LDA_ABS, 0x6380, 0x6340); + PatchAddress(LDA_ABS, 0x6381, 0x6341); + PatchAddress(STA_ABS, 0x6380, 0x6340); + PatchAddress(STA_ABS, 0x6381, 0x6341); var asm = engine.Module(); asm.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.z2ft.s"), "z2ft.s"); } diff --git a/Statistics/Statistics.csproj b/Statistics/Statistics.csproj index 49d6813ce..5ed9a1bbc 100644 --- a/Statistics/Statistics.csproj +++ b/Statistics/Statistics.csproj @@ -1,6 +1,6 @@  - net8.0-windows + net10.0-windows Exe Z2Randomizer.Statistics AnyCPU;x64 diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index cae44bc06..905b9b64e 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,7 +1,7 @@  - net8.0-windows + net10.0-windows enable enable From 83419c6ff36a6e2e7596d75b0052d66da3b255d8 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 28 Apr 2026 02:02:16 -0400 Subject: [PATCH 018/128] Work around NMI interrupting audio playback --- RandomizerCore/Asm/MMC5.s | 14 +++++++------- RandomizerCore/Asm/z2ft.s | 11 ++++++++++- RandomizerCore/Asm/z2r.inc | 1 + 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/RandomizerCore/Asm/MMC5.s b/RandomizerCore/Asm/MMC5.s index f0fa5721b..60b14374a 100644 --- a/RandomizerCore/Asm/MMC5.s +++ b/RandomizerCore/Asm/MMC5.s @@ -201,13 +201,13 @@ HandleLagFrame: ; bne @loop jsr SetupScanlineIRQ @HandleAudio: - ; Skip processing audio during a real lag frame since thats what - ; vanilla accomplishes with a hard disabled NMI -; lda SoftDisableNmi -; bne @skip - jmp UpdateSound -;@skip: -; rts + lda PendingAudioCall + beq @RunAudio + ; Audio is currently running on the main thread; signal it to re-run after + dec PendingAudioCall + rts +@RunAudio: + jmp UpdateSound .segment "PRG7" diff --git a/RandomizerCore/Asm/z2ft.s b/RandomizerCore/Asm/z2ft.s index 8229de4c7..429888efa 100644 --- a/RandomizerCore/Asm/z2ft.s +++ b/RandomizerCore/Asm/z2ft.s @@ -12,6 +12,9 @@ UpdateSound = $878c .export CallUpdateSound .proc CallUpdateSound + lda #1 + sta PendingAudioCall + ; Must preserve these because in Z2R it's possible to be interrupted by NMI that may change banks. lda NmiBankShadow8 pha @@ -19,7 +22,6 @@ UpdateSound = $878c pha lda #6 - jsr SwapPRG jsr UpdateSound @@ -31,6 +33,13 @@ UpdateSound = $878c sta NmiBankShadow8 sta PrgBank8Reg + ; If a lag-frame NMI decremented PendingAudioCall to 0 while audio was running, re-run now + lda PendingAudioCall + bne @NoPending + jmp CallUpdateSound +@NoPending: + lda #0 + sta PendingAudioCall rts .endproc ; CallUpdateSound diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 99bcaddda..f1da677c7 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -180,6 +180,7 @@ RESV DripperRedCounter RESV PpuCtrlForIrq RESV ScrollPosForIrq RESV IrqSetupAlready +RESV PendingAudioCall ; 1=audio running on main thread, 0=NMI decremented it (needs re-run) ; Expanded the block break list to hold up to 10 bricks at a time instead of just 5 BREAKABLE_BLOCK_COUNT = 10 RESV BlockBreakYCoord, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0429 - Y Low byte for collision table From db486fa53f9bba7c9fca3c06e7c1d868ce4d6dad Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sun, 3 May 2026 22:07:29 -0400 Subject: [PATCH 019/128] WIP boss fight collision and proper motion --- RandomizerCore/Asm/z2mario/boss.s | 107 +++++++++++++++++++---- RandomizerCore/Asm/z2mario/integration.s | 5 +- RandomizerCore/Asm/z2mario/mario.s | 6 +- 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index 7114ff2c6..7f9c7c0ca 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -3,13 +3,16 @@ .include "z2r.inc" .import METASPRITE_BIG_MARIO_SKIDDING, METASPRITE_BIG_MARIO_JUMPING, METASPRITE_FIRE_MARIO_STANDING, METASPRITE_BIG_MARIO_STANDING -.import DrawMetasprite +.import DrawMetasprite, METASPRITE_BIG_MARIO_WALKING_1 .import SwapPRG, SwapToSavedPRG, SwapToPRG0 +.import METASPRITE_BIG_MARIO_CROUCHING +CROUCHING = METASPRITE_BIG_MARIO_CROUCHING SKIDDING = METASPRITE_BIG_MARIO_SKIDDING JUMPING = METASPRITE_BIG_MARIO_JUMPING SHOOT = METASPRITE_FIRE_MARIO_STANDING STANDING = METASPRITE_BIG_MARIO_STANDING +WALKING = METASPRITE_BIG_MARIO_WALKING_1 mus_bossdie = 8 ; Boss RAM size for clearing loop @@ -35,11 +38,17 @@ PatchBossInit: .reloc BossLandedInCutscene: sta $0505 -.import METASPRITE_BIG_MARIO_CROUCHING - lda #METASPRITE_BIG_MARIO_CROUCHING + lda #CROUCHING sta boss_animation rts +.segment "PRG5" +.org $988e + jsr RunBoss + jmp $9a77 +FREE_UNTIL $9a77 + +.segment "PRG5", "PRG7" .reloc InitShadowBoss: ; Clear all boss RAM @@ -65,8 +74,56 @@ InitShadowBoss: sta Enemy_MovingDir,x rts + +.segment "PRG7" +.import BlockBufferCollision +.reloc +; Trimmed down version that works just well enough for what i need +EnemyToBGCollisionDet: + lda NmiBankShadow8 + pha + lda NmiBankShadowA + pha + + lda #0 + jsr SwapPRG + + lda #0 ;set flag in A for save vertical coordinate + ldy #1 ;set Y to check the bottom middle of enemy object (big mario's feet position) + ldx ObjectOffset ;get object offset converted to enemy offset + inx + jsr BlockBufferCollision ;do collision detection subroutine for sprite object + tax + + pla + sta NmiBankShadowA + sta PrgBankAReg + pla + sta NmiBankShadow8 + sta PrgBank8Reg + cpx #$00 ;check to see if object bumped into anything + bne EnemyLanding + rts + +EnemyLanding: + ldx ObjectOffset ;get object offset + lda #$00 ;initialize vertical speed + sta Enemy_Y_Speed,x ;and movement force + sta Enemy_Y_MoveForce,x + lda Enemy_State,x + and #%10111111 ; land the boss on the ground + sta Enemy_State,x + lda Enemy_Y_Position,x + and #%11110000 ;save high nybble of vertical coordinate, and + ; ora #%00001000 ;set d3, then store, probably used to set enemy object + sta Enemy_Y_Position,x ;neatly on whatever it's landing on + rts + +.segment "PRG5", "PRG7" .reloc RunBoss: + lda #0 + sta $de ; unfreeze link lda boss_freeze beq @notfreezed dec boss_freeze @@ -109,7 +166,7 @@ RunBoss: lda TimerControl bne @nograv ; skip movement if timer frozen -; jsr EnemyToBGCollisionDet ; background collision + jsr EnemyToBGCollisionDet ; background collision jsr HandleBossMovement ; AI state machine lda Enemy_State,x @@ -128,6 +185,9 @@ RunBoss: ; jsr GetEnemyOffscreenBits ; jsr GetEnemyBoundBox ; jmp PlayerEnemyCollision ; check player<->boss collision + ; JSR $9A8D ; load collision hitbox (originally loads sword hitbox?) + jsr $9a97 ; trying random hitboxes??? + JSR $E4D9 ; do collision detection rts @decrement_timer: @@ -375,17 +435,17 @@ MovGoToMiddle: @facing_right: ; Facing right: target $40 lda Enemy_X_Position,x - cmp #$40 + cmp #$80 bcs @arrived @keep_walking: ; Set walk speed based on direction lda Enemy_MovingDir,x lsr bcs @walk_right - lda #<(-2) + lda #<(-10) .byte $2c ; skip next instruction @walk_right: - lda #2 + lda #10 sta Enemy_X_Speed,x jmp MoveEnemyHorizontally @@ -414,9 +474,9 @@ BossSetWalkingAnim: bne @init_anim inc boss_walking_anim lda boss_walking_anim - cmp #$03 + cmp #WALKING+3 bne @init_anim - lda #0 + lda #WALKING sta boss_walking_anim @init_anim: lda boss_walking_anim @@ -565,7 +625,7 @@ KillBoss: ; sta AreaMusicQueue dec boss_counter -; jsr EnemyToBGCollisionDet + jsr EnemyToBGCollisionDet lda #STANDING sta boss_animation lda Enemy_State,x @@ -657,14 +717,31 @@ DamagePlayer: ora #$10 ; set flag indicating link was hit by this enemy? sta $a8,x rts + +.segment "PRG7" .reloc .import ImposeGravitySprObj MoveD_EnemyVertically: - ldy #$3d ;set quick movement amount downwards -SetHiMax: lda #$03 ;set maximum speed in A -SetXMoveAmt: sty R0 ;set movement amount here - inx ;increment X for enemy offset - jsr ImposeGravitySprObj ;do a sub to move enemy object downwards + lda NmiBankShadow8 + pha + lda NmiBankShadowA + pha + + lda #0 + jsr SwapPRG + + ldy #$3d ;set quick movement amount downwards + lda #$03 ;set maximum speed in A + sty R0 ;set movement amount here + inx ;increment X for enemy offset + + jsr ImposeGravitySprObj ;do a sub to move enemy object downwards + pla + sta NmiBankShadowA + sta PrgBankAReg + pla + sta NmiBankShadow8 + sta PrgBank8Reg ldx ObjectOffset ;get enemy object buffer offset and leave rts diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 3e342580f..e88b5ccbb 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -86,7 +86,7 @@ PatchLinkLivesScreenDraw: jsr BankSwitchMarioCHR jmp $EC02 -.org $CAe9 +.org $cae9 jsr AlsoResetPlayerSizeOnContinue .reloc AlsoResetPlayerSizeOnContinue: @@ -153,6 +153,9 @@ DrawFallingMarioSprite: nop .segment "PRG7" +.org $dd1b ; TBird draws a flashing mario sprite here without banking + jsr BankPatchLinkDrawRoutine + .reloc BankPatchLinkDrawRoutine: lda $0769 diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 07aa6beb6..38c3ab258 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -222,8 +222,8 @@ PlayerSubs: ldy #$1d ; Jump to the middle of the original link collision routine ; jsr $e070 ; link original bg collision routine - jsr SlightlyModifiedCollisionRoutine - rts + jmp SlightlyModifiedCollisionRoutine + ; rts ; lda Player_Y_Position ; cmp #$40 ;check to see if player is higher than 64th pixel @@ -1774,7 +1774,7 @@ BlockBuffer_Y_Adder: .byte $12, $20, $20, $18, $18, $18, $18 ; small/crouching .byte $18, $14, $14, $06, $06, $08, $10 ; misc - +.export BlockBufferCollision BlockBufferColli_Feet: iny ;if branched here, increment to next set of adders BlockBufferColli_Head: From 88bf6ef7ab88a2e4991dd9b03fbc8eb966bcbdb7 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 7 May 2026 22:49:50 -0400 Subject: [PATCH 020/128] Mostly working boss. New title screen logo --- RandomizerCore/Asm/z2mario/boss.s | 87 ++++++++++++++++++--- RandomizerCore/Asm/z2mario/integration.s | 71 ++++++----------- RandomizerCore/Asm/z2mario/mario_title.chr | Bin 0 -> 1024 bytes RandomizerCore/Hyrule.cs | 11 ++- RandomizerCore/ROM.cs | 56 +++++++++---- RandomizerCore/RandomizerCore.csproj | 2 + 6 files changed, 154 insertions(+), 73 deletions(-) create mode 100644 RandomizerCore/Asm/z2mario/mario_title.chr diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index 7f9c7c0ca..fc3bbdeab 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -6,6 +6,8 @@ .import DrawMetasprite, METASPRITE_BIG_MARIO_WALKING_1 .import SwapPRG, SwapToSavedPRG, SwapToPRG0 +.export InjuredBoss + .import METASPRITE_BIG_MARIO_CROUCHING CROUCHING = METASPRITE_BIG_MARIO_CROUCHING SKIDDING = METASPRITE_BIG_MARIO_SKIDDING @@ -48,6 +50,16 @@ BossLandedInCutscene: jmp $9a77 FREE_UNTIL $9a77 +; Patch bank5_Enemy_Vulnerability_Damage_Codes entry $23 (Dark Link): +; Clear bit 5 "Immune to Flying Blade and Fire" so fireballs can hit the boss. +.org $951C + .byte $14 + +; Make dark mario use a different hitbox (vanilla has a jank $0e hitbox, switch +; to a simple tall enemy hitbox with $00) +.org $9540 + .byte $00 + .segment "PRG5", "PRG7" .reloc InitShadowBoss: @@ -58,9 +70,9 @@ InitShadowBoss: sta boss_animation,x dex bpl @loop - ldx #1 + ldx ObjectOffset ; Set HP - lda #20 + lda #120 sta Enemy_HP,x lda #JUMPING sta boss_animation @@ -180,15 +192,7 @@ RunBoss: ; Collision detection (only when not invincible) lda boss_invincibility_timer bne @decrement_timer -; lda #$0a -; sta Enemy_BoundBoxCtrl,x ; set bounding box size -; jsr GetEnemyOffscreenBits -; jsr GetEnemyBoundBox -; jmp PlayerEnemyCollision ; check player<->boss collision - ; JSR $9A8D ; load collision hitbox (originally loads sword hitbox?) - jsr $9a97 ; trying random hitboxes??? - JSR $E4D9 ; do collision detection - rts + jmp BossPlayerCollision @decrement_timer: dec boss_invincibility_timer @@ -718,6 +722,67 @@ DamagePlayer: sta $a8,x rts +; Vanilla hitbox primitive addresses (all in fixed PRG7, callable from anywhere) +bank7_LoadObjectHitbox = $E942 ; loads enemy hitbox into ZP $04-$07 using Enemy_X/Y_Position,x +bank7_LoadLinkHitbox = $E975 ; loads Link/Mario body hitbox into ZP $00-$03 +bank7_CollisionTest = $E9F9 ; rectangle intersection; Carry set = overlap +bank7_LinkCollision = $D6C1 ; processes $A8,x enemy-hit flags -> calls link hurt routine +bank7_Sword_Hit_Detection = $E677 ; check if the HitboxX/Y overlaps with the current enemy +.reloc +; Check contact between Mario and the boss each frame. +BossPlayerCollision: + ldx ObjectOffset + + jsr bank7_LoadObjectHitbox ; boss body hitbox -> ZP $04-$07 + jsr bank7_Sword_Hit_Detection ; compared with the sword stab routine + bcs @hit_boss + + jsr bank7_LoadLinkHitbox ; Mario body hitbox -> ZP $00-$03 + jsr bank7_CollisionTest + bcs @contact_damage + rts + +@hit_boss: + jsr InjuredBossJump + lda #$fd + sta Player_Y_Speed ; bounce Mario up +@no_collision: + rts + +@contact_damage: + ; Mario touched boss without downstabbing: hurt Mario via vanilla damage system + lda $a8,x + ora #$10 ; enemy-contacted-link flag + sta $a8,x + jmp bank7_LinkCollision ; processes $A8 flag to damage Mario; returns to RunBoss caller + + +.reloc +; Variant of InjuredBoss that subtracts 5 HP at once (jump attack damage). +; HP is floored at 0 so the KillBoss death-countdown path runs normally. +InjuredBossJump: + lda boss_invincibility_timer + bne @done ; already invincible, no damage (caller still bounces Mario) + inc boss_screech + lda Enemy_HP,x + sec + sbc #5 + bcs + + lda #0 ; floor at 0 ++: + sta Enemy_HP,x + lda #$10 ; Sword stab SFX + sta Z2NoiseSoundQueue ; stab SFX + lda #30 ; 0.5 seconds at 60 fps + sta boss_invincibility_timer + ; Reset state timer if currently idle (mirrors InjuredBoss behavior) + lda boss_state + bne @done + lda #0 + sta boss_state_timer +@done: + rts + .segment "PRG7" .reloc .import ImposeGravitySprObj diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index e88b5ccbb..836e2e048 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -5,7 +5,6 @@ .import ProcHammerTime .import Square1SfxHandler, Square2SfxHandler, NoiseSfxHandler .import SwapToSavedPRG, SwapToPRG0 - .export SlightlyModifiedCollisionRoutine ; Remove unused code after we gutted link's movement @@ -553,9 +552,8 @@ NoDecTimers: jsr SetPlayerDownstabbingHitbox ; jsr CheckSideviewTransition ; this is already run during collision detection - jsr PlayerGfxHandler - - rts + jmp PlayerGfxHandler + ; rts .reloc SetPlayerDownstabbingHitbox: @@ -623,31 +621,25 @@ CheckHammerHitboxes: jsr PatchFireballHitcheck .reloc PatchFireballHitcheck: - ldy $11 ; fireball or hammer - ; if its a hammer let it pass through + ldy $11 ; fireball or hammer slot (0 or 1) lda Fireball_State,y and #%01000000 - beq @IsFireball - ; Is Hammer - ; Load the X / Y coord into $47e and $480 - ; use a "sword" type of attack for the hitbox check - lda #0 - .byte $2c ; skips 2 bytes -; jsr $e67e ; original hitbox check for swords (skipping past sword offscreen check) -; jmp @AfterCheck - @IsFireball: - lda #1 - sta $0b ; Holds the "type" of attack (0 = sword, 1 = projectile) - jsr $E694 ; original hitbox check for projectiles - bcc @exit - ldy $11 ; fireball or hammer - ; if its a hammer let it pass through - lda Fireball_State,y - and #%01000000 - bne @exit - ; destroy the fireball - lda #%10000000 - sta Fireball_State,y + bne @IsHammer + lda #1 + sta $0b ; 1 = fireball + bne @DoCheck ; always taken +@IsHammer: + lda #0 + sta $0b ; 0 = hammer (pass-through) +@DoCheck: + jsr $E694 + bcc @exit + ldy $11 + lda Fireball_State,y + and #%01000000 + bne @exit ; hammer: pass through + lda #%10000000 + sta Fireball_State,y @exit: rts @@ -882,6 +874,8 @@ ClearQueues: .reloc CheckMarioSq1Sfx: + lda $07E1 ; L990B active effect type (non-zero = L990B is driving Square 1) + bne @Z2ExtraActive jsr Square1SfxHandler lda Square1SoundBuffer beq @PlayingZ2 @@ -896,6 +890,7 @@ CheckMarioSq1Sfx: ; but now we can clear it. lda #0 sta Z2Square1SoundBuffer +@Z2ExtraActive: rts @ActualZ1Sfx: jmp $92F4 ; Original square 1 sfx from z2 @@ -920,6 +915,8 @@ CheckMarioSq2Sfx: jmp $9408 ; Original square 2 sfx from z2 .reloc CheckMarioNoiseSfx: + lda $07E1 ; L990B active effect type (non-zero = L990B is driving Noise) + bne @Z2ExtraActive jsr NoiseSfxHandler lda NoiseSoundBuffer beq @PlayingZ2 @@ -934,27 +931,11 @@ CheckMarioNoiseSfx: ; but now we can clear it. lda #0 sta Z2NoiseSoundBuffer +@Z2ExtraActive: rts @ActualNoiseSfx: jmp $95A7 ; Original "complex" sfx from z2 -; .reloc -; CheckMarioSfx: -; lda Square1SoundQueue -; ora MarioSquare1SoundBuffer -; ora Square2SoundQueue -; ora MarioSquare2SoundBuffer -; ora NoiseSoundQueue -; ora MarioNoiseSoundBuffer -; beq @NoMarioSfx -; jsr SFXSoundEngine -; sta MarioSquare1SoundPlaying -; jmp $901C -; @NoMarioSfx: -; ; P -; jsr $92F4 ; Zelda 2 square 1 sfx processing -; rts - .segment "PRG7" ; Make mario state "on ground" when riding an elevator .org $d8cd @@ -978,7 +959,7 @@ ElevatorMakeMarioStateStanding: SetLinkRecoil = $e371 -.reloc +; .reloc ;DisableRecoilIfDownstab: ; jsr CheckForDownstab ; bcs + diff --git a/RandomizerCore/Asm/z2mario/mario_title.chr b/RandomizerCore/Asm/z2mario/mario_title.chr new file mode 100644 index 0000000000000000000000000000000000000000..1f9233dcfb35c01a94cc0cb2e8f2a406bc3b18d0 GIT binary patch literal 1024 zcmcgrv2NQy45fUA&V!Isk;Ow`2^}+e^k7)-5A+-K1w}>=LZw(k$No*nLWYNo{RJ5r zysF3$6~5~7$a4($2l@t4kK|E&Jc@`Y2$X`kpa(n&L$=x^*(HdVrfDkA=MQX?B>N{` z0Y0ARY%{=f|Lg|muDLPh`!yc`&x3$%ppChXTjzeA`5e}aM%(r({^S1sMYd_l?Nr;i7pc6BA=(E6e4+7?1f_xgG~K0OglCe!I+0T-i9(>Wtv z7C>2+ih+UXL8ZP#@ur5+jZfBur^dE3Ix)9rg>#*8rWc-I%#qPVJx;ke&5>owAg zVzo+$#@p>~m!lM4mxdJ-eJ`CV7CM_P2iteQkq{_tZU$=$i;tm~8VT^+pt~$93+t~) zsc+EloKhR9W-VHjF5@Xos%dh$f2!+!i>r|3aaWopq zJdDv=?GJ4nPbX0nY8-02esFo3#{5YyKVvQ(5b^e)7PRxkuzS$%!MV+OwU(RByKV`h zK?u}zI6MoV-QE2?olXSp!Z97(ie(i(UtSf-HNg`_k;oLO;K<{_5Zagg`C`ga==tSE wIQBu}oO6b73{U9o?FW?ZsLcG;R!!&lomWE=0+ryv`9CAVxyxTA@&ELA1O7V^ZvX%Q literal 0 HcmV?d00001 diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index e214443cc..76078f8cf 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2430,6 +2430,9 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra rom.Put(ROM.ChrRomOffset + 0x1a800, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.sprites_mario_hammer.chr")); var span = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.map_mario.chr"); rom.Put(ROM.ChrRomOffset + 0x11a00, span[0..0x220]); + var title_graphics = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.mario_title.chr"); + rom.Put(ROM.ChrRomOffset + 0x1380, title_graphics[0..0x300]); + rom.Put(ROM.ChrRomOffset + 0x1840, title_graphics[0x300..0x400]); } if (props.EncounterRates == EncounterRate.NONE) @@ -3820,7 +3823,7 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando bool randomizeMusic = !props.DisableMusic && props.RandomizeMusic; ChangeMapperToMMC5(engine, props.DisableHUDLag, randomizeMusic, props.MarioMode); // will make output vary with customize tab options - rom.AddRandomizerToTitle(engine); + rom.AddRandomizerToTitle(engine, props.MarioMode); AddCropGuideBoxesToFileSelect(engine); FixHelmetheadBossRoom(engine); FullItemShuffle(engine, GetNonSideviewItemLocations()); @@ -3958,6 +3961,8 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando // recompile that, I'm just gonna patch it here... const byte LDA_ABS = 0xAD; const byte STA_ABS = 0x8D; + const byte LDX_ABS = 0xAE; + const byte STX_ABS = 0x8E; void PatchAddress(byte opcode, int before, int after) { @@ -3974,6 +3979,10 @@ void PatchAddress(byte opcode, int before, int after) PatchAddress(LDA_ABS, 0x6381, 0x6341); PatchAddress(STA_ABS, 0x6380, 0x6340); PatchAddress(STA_ABS, 0x6381, 0x6341); + PatchAddress(LDX_ABS, 0x6380, 0x6340); + PatchAddress(LDX_ABS, 0x6381, 0x6341); + PatchAddress(STX_ABS, 0x6380, 0x6340); + PatchAddress(STX_ABS, 0x6381, 0x6341); var asm = engine.Module(); asm.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.z2ft.s"), "z2ft.s"); } diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index fb8435956..3a5407ede 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -434,11 +434,48 @@ public void WritePalacePalettes(List bricks, List curtains, List + + From d15208023c35b6b963b7ae9e9feb7be74443c3e3 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 8 May 2026 04:09:46 -0400 Subject: [PATCH 021/128] working towards finally getting the final boss done --- RandomizerCore/Asm/z2mario/boss.s | 163 +++++++++++++---------- RandomizerCore/Asm/z2mario/integration.s | 39 +++++- RandomizerCore/Asm/z2mario/metasprite.s | 16 ++- 3 files changed, 134 insertions(+), 84 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index fc3bbdeab..4ee74fde6 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -5,6 +5,7 @@ .import METASPRITE_BIG_MARIO_SKIDDING, METASPRITE_BIG_MARIO_JUMPING, METASPRITE_FIRE_MARIO_STANDING, METASPRITE_BIG_MARIO_STANDING .import DrawMetasprite, METASPRITE_BIG_MARIO_WALKING_1 .import SwapPRG, SwapToSavedPRG, SwapToPRG0 +.import METASPRITE_SMALL_MARIO_DEATH .export InjuredBoss @@ -31,7 +32,7 @@ PatchBossInit: sta EnemyYVelocity jmp InitShadowBoss -; Patch the location where it draws the boss +; Patch the location where it draws the boss (effectively a nop) .org $9a7f jsr BossGraphicsHandler @@ -70,9 +71,9 @@ InitShadowBoss: sta boss_animation,x dex bpl @loop - ldx ObjectOffset + ldx #0 ; Dark Link entity is always at enemy slot 0 ; Set HP - lda #120 + lda #60 sta Enemy_HP,x lda #JUMPING sta boss_animation @@ -81,7 +82,7 @@ InitShadowBoss: inc ReloadCHRBank ; Face left - ldx ObjectOffset + ldx #0 lda #2 sta Enemy_MovingDir,x rts @@ -102,8 +103,8 @@ EnemyToBGCollisionDet: lda #0 ;set flag in A for save vertical coordinate ldy #1 ;set Y to check the bottom middle of enemy object (big mario's feet position) - ldx ObjectOffset ;get object offset converted to enemy offset - inx + ldx #0 ;boss is always enemy slot 0 + inx ;add EnemyOffset=1: SprObject index for slot 0 jsr BlockBufferCollision ;do collision detection subroutine for sprite object tax @@ -118,7 +119,7 @@ EnemyToBGCollisionDet: rts EnemyLanding: - ldx ObjectOffset ;get object offset + ldx #0 ;boss is always at enemy slot 0 lda #$00 ;initialize vertical speed sta Enemy_Y_Speed,x ;and movement force sta Enemy_Y_MoveForce,x @@ -146,9 +147,9 @@ RunBoss: lsr bcs @nodraw2 ; jsr RelativeEnemyPosition - jsr BossGraphicsHandler + ; jsr BossGraphicsHandler @nodraw2: - ldx ObjectOffset + ldx #0 rts @notfreezed: @@ -164,16 +165,16 @@ RunBoss: ; sta EnemyFrenzyBuffer ; suppress enemy frenzy spawns ; Draw (skip every other frame if invincible for blink effect) - lda boss_invincibility_timer - beq @draw - lda FrameCounter - lsr - bcs @nodraw - @draw: +; lda boss_invincibility_timer +; beq @draw +; lda FrameCounter +; lsr +; bcs @nodraw +; @draw: ; jsr RelativeEnemyPosition - jsr BossGraphicsHandler + ; jsr BossGraphicsHandler @nodraw: - ldx ObjectOffset + ldx #0 lda TimerControl bne @nograv ; skip movement if timer frozen @@ -548,7 +549,7 @@ BossSpawnFireball: sta Enemy_MovingDir,y lda #5 ; TODO What to spawn here? BowserFlame jsr BossSpawnEnemy - ldx ObjectOffset + ldx #0 @done: rts @@ -594,7 +595,7 @@ BossSpawnSideBullet: tay pla sta Enemy_MovingDir,y - ldx ObjectOffset + ldx #0 @done: rts @@ -623,37 +624,44 @@ BossSpawnEnemy: .reloc KillBoss: + ; First frame: boss_counter still equals 80 (reset each alive frame) lda boss_counter - bmi @death_complete ; counter expired -> signal fully dead -; lda #Silence -; sta AreaMusicQueue + cmp #80 + bne @animating + ; Initialize death animation: shrink to small mario death pose and launch upward + lda #METASPRITE_SMALL_MARIO_DEATH + sta boss_animation + lda #CHR_SMALLMARIO + sta boss_ChrBank + inc ReloadCHRBank + lda #<(-20) ; fast upward launch (speed = -20) + sta Enemy_Y_Speed,x + lda #0 + sta Enemy_X_Speed,x ; stop horizontal movement + lda #150 ; ~2.5 second animation window + sta boss_counter + ; vanilla boss kill behavior + lda #$C0 + sta $074B ; set flash timer + lda #$04 ; set sfx + sta $EC + lda #$14 ; force link to the ground? + sta $0751 + lda $4D + sta $4E + lda $29 + sta $2A + lda #$FF + sta $504 ; stop link from moving + @animating: + jsr MoveD_EnemyVertically ; gravity decelerates upward motion then pulls boss down dec boss_counter - - jsr EnemyToBGCollisionDet - lda #STANDING - sta boss_animation - lda Enemy_State,x - and #%01000000 - beq @nograv - jsr MoveD_EnemyVertically - lda #JUMPING - sta boss_animation - @nograv: - ; Draw every other frame (flash effect) - lda FrameCounter - lsr - bcs @nodraw -; jsr RelativeEnemyPosition - jsr BossGraphicsHandler - @nodraw: - ldx ObjectOffset - rts - - @death_complete: - ; Counter expired: signal death complete - dec Enemy_HP,x ; HP goes to $FF (negative) -; lda #mus_bossdie -; sta AreaMusicQueue + bpl @done ; still in animation window + dec Enemy_HP,x + @done: + lda #1 + sta $0505 + ldx #0 rts ;.reloc @@ -723,38 +731,44 @@ DamagePlayer: rts ; Vanilla hitbox primitive addresses (all in fixed PRG7, callable from anywhere) -bank7_LoadObjectHitbox = $E942 ; loads enemy hitbox into ZP $04-$07 using Enemy_X/Y_Position,x -bank7_LoadLinkHitbox = $E975 ; loads Link/Mario body hitbox into ZP $00-$03 -bank7_CollisionTest = $E9F9 ; rectangle intersection; Carry set = overlap -bank7_LinkCollision = $D6C1 ; processes $A8,x enemy-hit flags -> calls link hurt routine -bank7_Sword_Hit_Detection = $E677 ; check if the HitboxX/Y overlaps with the current enemy +bank7_LoadObjectHitbox = $E942 ; loads enemy hitbox into ZP $04-$07 using Enemy_X/Y_Position,x +bank7_LoadLinkHitbox = $E975 ; loads Link/Mario body hitbox into ZP $00-$03 +bank7_CollisionTest = $E9F9 ; rectangle intersection; Carry set = overlap +bank7_LinkCollision = $D6C1 ; processes $A8,x enemy-hit flags -> calls link hurt routine .reloc ; Check contact between Mario and the boss each frame. BossPlayerCollision: - ldx ObjectOffset - - jsr bank7_LoadObjectHitbox ; boss body hitbox -> ZP $04-$07 - jsr bank7_Sword_Hit_Detection ; compared with the sword stab routine - bcs @hit_boss + ldx #0 + + jsr bank7_LoadObjectHitbox ; boss body hitbox into ZP $04-$07 + + ; Stomp detection: Mario must be falling (Player_Y_Speed > 0 in SMB1 convention). + ; $00 = on the ground, $80-$FF = ascending, $01-$7F = falling. + lda Player_Y_Speed + beq @try_body_contact ; standing – not a stomp + bmi @try_body_contact ; going up – not a stomp + ; Mario is falling: check body overlap for stomp + jsr bank7_LoadLinkHitbox ; Mario body hitbox into ZP $00-$03 + jsr bank7_CollisionTest ; carry set = overlap + bcc @no_contact + jsr InjuredBossJump + lda #$fd + sta Player_Y_Speed ; bounce Mario up + rts - jsr bank7_LoadLinkHitbox ; Mario body hitbox -> ZP $00-$03 +@try_body_contact: + jsr bank7_LoadLinkHitbox ; Mario body hitbox into ZP $00-$03 jsr bank7_CollisionTest bcs @contact_damage - rts - -@hit_boss: - jsr InjuredBossJump - lda #$fd - sta Player_Y_Speed ; bounce Mario up -@no_collision: +@no_contact: rts @contact_damage: - ; Mario touched boss without downstabbing: hurt Mario via vanilla damage system + ; Mario touched boss without stomping: hurt Mario via vanilla damage system lda $a8,x - ora #$10 ; enemy-contacted-link flag + ora #$10 ; enemy-contacted-link flag sta $a8,x - jmp bank7_LinkCollision ; processes $A8 flag to damage Mario; returns to RunBoss caller + jmp bank7_LinkCollision ; processes $A8 flag to damage Mario; returns to RunBoss caller .reloc @@ -769,10 +783,13 @@ InjuredBossJump: sbc #5 bcs + lda #0 ; floor at 0 -+: ++ sta Enemy_HP,x - lda #$10 ; Sword stab SFX - sta Z2NoiseSoundQueue ; stab SFX + beq + + ; don't play sfx for last hit + lda #$10 ; Sword stab SFX + sta Z2NoiseSoundQueue ; stab SFX + + lda #30 ; 0.5 seconds at 60 fps sta boss_invincibility_timer ; Reset state timer if currently idle (mirrors InjuredBoss behavior) @@ -807,7 +824,7 @@ MoveD_EnemyVertically: pla sta NmiBankShadow8 sta PrgBank8Reg - ldx ObjectOffset ;get enemy object buffer offset and leave + ldx #0 ;boss is always at enemy slot 0 rts ;.reloc diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 836e2e048..d34883e21 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -5,6 +5,7 @@ .import ProcHammerTime .import Square1SfxHandler, Square2SfxHandler, NoiseSfxHandler .import SwapToSavedPRG, SwapToPRG0 +.import InjuredBoss .export SlightlyModifiedCollisionRoutine ; Remove unused code after we gutted link's movement @@ -217,6 +218,13 @@ PatchLinkDrawRoutine: + @skipplayer: ; Check if we are rendering the Mario Boss + lda boss_invincibility_timer + beq + + ; if the boss is flickering skip drawing every other frame + lda FrameCounter + lsr + bcs @noboss + + ldx boss_animation beq @noboss ldy #1 @@ -583,6 +591,8 @@ SetPlayerDownstabbingHitbox: LoadProjectileCollisionBox = $e4bc SwordCollisionCheck = $E677 BreakBlockCollisionCheck = $e1e6 +bank7_LoadObjectHitbox = $E942 +bank7_CollisionTest = $E9F9 .org $e1e0 ; Check the hammer hitboxes as if they were stabbing blocks jsr CheckHammerHitboxes @@ -627,20 +637,39 @@ PatchFireballHitcheck: bne @IsHammer lda #1 sta $0b ; 1 = fireball - bne @DoCheck ; always taken + bne @CheckBoss ; always taken @IsHammer: lda #0 sta $0b ; 0 = hammer (pass-through) -@DoCheck: +@CheckBoss: + ; Boss entity (type $23): bypass $E694 to avoid vanilla bank7_monster_death on HP=0 + lda $a1,x + cmp #$23 + bne @NotBoss + lda $0b + beq @exit ; hammer doesn't damage boss + jsr bank7_LoadObjectHitbox ; load boss hitbox ZP $04-$07 + jsr bank7_CollisionTest ; test fireball ($00-$03) vs boss ($04-$07) + bcc @exit ; miss + ldy $11 + lda Fireball_State,y + and #%01000000 + bne @exit ; hammer safety + lda #%10000000 + sta Fireball_State,y + jsr InjuredBoss ; decrement HP via controlled path, start invincibility +@exit: + rts +@NotBoss: jsr $E694 - bcc @exit + bcc @NotBossExit ldy $11 lda Fireball_State,y and #%01000000 - bne @exit ; hammer: pass through + bne @NotBossExit ; hammer: pass through lda #%10000000 sta Fireball_State,y -@exit: +@NotBossExit: rts ; Skip over stopping the projectile diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index fb08113d9..50a33326f 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -442,12 +442,16 @@ SetYOffset: beq @isplayer ; Rendering dark mario so use the dark mario pattern table ldx CurrentOAMOffset - lda #%11000000 - ora Sprite_Tilenumber - 16,x - sta Sprite_Tilenumber - 16,x - lda #%11000000 - ora Sprite_Tilenumber - 12,x - sta Sprite_Tilenumber - 12,x + lda boss_animation + cmp #METASPRITE_SMALL_MARIO_DEATH + beq @smallmario + lda #%11000000 + ora Sprite_Tilenumber - 16,x + sta Sprite_Tilenumber - 16,x + lda #%11000000 + ora Sprite_Tilenumber - 12,x + sta Sprite_Tilenumber - 12,x + @smallmario: lda #%11000000 ora Sprite_Tilenumber - 8,x sta Sprite_Tilenumber - 8,x From 4fa285ea0b9aec708035a7f1688cf7e3ee98207f Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 8 May 2026 14:47:25 -0400 Subject: [PATCH 022/128] Fix up boss death animation --- RandomizerCore/Asm/z2mario/boss.s | 55 +- RandomizerCore/Asm/z2mario/boss_reference.md | 852 ------------------- RandomizerCore/Asm/z2mario/integration.s | 13 +- 3 files changed, 16 insertions(+), 904 deletions(-) delete mode 100644 RandomizerCore/Asm/z2mario/boss_reference.md diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index 4ee74fde6..0bc864507 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -159,8 +159,8 @@ RunBoss: jmp KillBoss ; HP == 0: playing death countdown @alive: - lda #80 - sta boss_counter ; reset death countdown each frame while alive + ; lda #80 + ; sta boss_counter ; reset death countdown each frame while alive ; lda #0 ; sta EnemyFrenzyBuffer ; suppress enemy frenzy spawns @@ -624,9 +624,7 @@ BossSpawnEnemy: .reloc KillBoss: - ; First frame: boss_counter still equals 80 (reset each alive frame) lda boss_counter - cmp #80 bne @animating ; Initialize death animation: shrink to small mario death pose and launch upward lda #METASPRITE_SMALL_MARIO_DEATH @@ -634,69 +632,30 @@ KillBoss: lda #CHR_SMALLMARIO sta boss_ChrBank inc ReloadCHRBank - lda #<(-20) ; fast upward launch (speed = -20) + lda #<(-6) ; fast upward launch sta Enemy_Y_Speed,x lda #0 sta Enemy_X_Speed,x ; stop horizontal movement - lda #150 ; ~2.5 second animation window + lda #$7f sta boss_counter ; vanilla boss kill behavior lda #$C0 sta $074B ; set flash timer lda #$04 ; set sfx sta $EC - lda #$14 ; force link to the ground? + lda #$14 sta $0751 - lda $4D - sta $4E - lda $29 - sta $2A lda #$FF sta $504 ; stop link from moving @animating: jsr MoveD_EnemyVertically ; gravity decelerates upward motion then pulls boss down dec boss_counter - bpl @done ; still in animation window - dec Enemy_HP,x + bne @done ; still in animation window + dec Enemy_HP,x @done: - lda #1 - sta $0505 ldx #0 rts -;.reloc -;AnimateKilledBoss: -; jsr EraseEnemyObject -; -; lda #3 -; sta pal_type ; set palette type for post-boss -; lda #2 -; sta PowerUpType -; lda #PowerUpObject -; sta Enemy_ID+5 -; lda #0 -; sta Enemy_PageLoc+5 -; sta Enemy_X_Speed+5 -; lda Enemy_X_Position,x -; sta Enemy_X_Position+5 ; spawn at boss's position -; lda #$01 -; sta Enemy_Y_HighPos+5 -; sta Enemy_Flag+5 -; lda Enemy_Y_Position,x -; sta Enemy_Y_Position+5 -; lda #$80 -; sta Enemy_State+5 -; lda #$03 -; sta Enemy_BoundBoxCtrl+5 -; -; lda #<(-3) -; sta Enemy_Y_Speed+5 ; powerup floats upward -; -; lda #Sfx_GrowPowerUp -; sta Square2SoundQueue -; ldx ObjectOffset -; rts - .reloc StompedBoss: lda Player_Y_Position diff --git a/RandomizerCore/Asm/z2mario/boss_reference.md b/RandomizerCore/Asm/z2mario/boss_reference.md deleted file mode 100644 index 0c1d5d965..000000000 --- a/RandomizerCore/Asm/z2mario/boss_reference.md +++ /dev/null @@ -1,852 +0,0 @@ -# Shadow Mario Boss - Porting Reference - -A "shadow Mario" boss that replaces the original Bowser fight. The boss walks, runs, skids, jumps, shoots fireballs, and spawns bullet bills from screen edges. It has an HP system with invincibility frames on hit, a multi-state AI, and a death sequence that spawns a powerup. - -The boss uses the SMB1 enemy object system (slot 0, indexed by `ObjectOffset`/X register) and is drawn as a 4-row, 8-tile (16x32 pixel) sprite using the player's CHR tiles. - ---- - -## 1. Custom RAM Variables - -These must be allocated in your target project. Originally located at `$7e12`-`$7e1c` + `$7e75`. - -| Variable | Original Addr | Description | -|---|---|---| -| `boss_animation` | `$7e12` | Current animation frame index (see constants below) | -| `boss_invincibility_timer` | `$7e13` | Countdown after being hit; boss blinks and is immune | -| `boss_screech` | `$7e14` | Incremented on each hit (tracks total damage taken) | -| `boss_state` | `$7e15` | AI state: 0=side/idle, 1=running, 2=middle | -| `boss_state_timer` | `$7e16` | Countdown for current state duration | -| `boss_walking_anim` | `$7e17` | Walking animation sub-counter (cycles 0-1-2) | -| `boss_bridge` | `$7e18` | Flag: jump while running (set when transitioning) | -| `boss_shootanim` | `$7e1a` | Shoot animation timer (decrements each draw frame) | -| `boss_substate` | `$7e1b` | Sub-state for middle action (0=moving to center, 1=acting) | -| `boss_counter` | `$7e1c` | Death countdown timer | -| `boss_freeze` | `$7e75` | Freeze timer (boss pauses, e.g. after certain events) | -| `bbSpeed` | `$7e00` | Bullet bill horizontal speed (per enemy slot, array) | - -Total custom RAM: ~12 bytes contiguous + 2 separate bytes. - ---- - -## 2. SMB1 Engine RAM Variables Used - -These are standard SMB1 object system variables. Your target project must provide equivalents. - -### Enemy Object Arrays (indexed by X or slot number) - -| Variable | Addr | Description | -|---|---|---| -| `Enemy_ID` | `$16` | Enemy type identifier | -| `Enemy_State` | `$1e` | State flags (bit 6 = falling/airborne) | -| `Enemy_Flag` | `$0f` | Nonzero = slot active | -| `Enemy_MovingDir` | `$46` | 1=right, 2=left | -| `Enemy_X_Speed` | `$58` | Horizontal speed (signed) | -| `Enemy_X_Position` | `$87` | X position (low byte) | -| `Enemy_PageLoc` | `$6e` | X position (page/high byte) | -| `Enemy_Y_Speed` | `$a0` | Vertical speed (signed) | -| `Enemy_Y_Position` | `$cf` | Y position (low byte) | -| `Enemy_Y_HighPos` | `$b6` | Y position (high byte) | -| `Enemy_X_MoveForce` | `$0401` | Horizontal sub-pixel force | -| `Enemy_Y_MoveForce` | `$0434` | Vertical sub-pixel force | -| `Enemy_BoundBoxCtrl` | `$049a` | Bounding box size selector | -| `Enemy_CollisionBits` | `$0491` | Collision result bits | -| `Enemy_SprAttrib` | `$03c5` | Sprite attribute byte | -| `Enemy_SprDataOffset` | `$06e5` | Offset into OAM sprite data | -| `Enemy_Rel_XPos` | `$03ae` | Screen-relative X (set by `RelativeEnemyPosition`) | -| `Enemy_Rel_YPos` | `$03b9` | Screen-relative Y | - -### Player Variables - -| Variable | Addr | Description | -|---|---|---| -| `Player_X_Position` | `$86` | Player X position | -| `Player_Y_Position` | `$ce` | Player Y position | -| `Player_Y_Speed` | `$9f` | Player vertical speed | -| `PlayerStatus` | `$0756` | 0=small, 1=big, 2=fire | -| `PlayerFacingDir` | `$33` | Player facing direction | - -### Game State - -| Variable | Addr | Description | -|---|---|---| -| `ObjectOffset` | `$08` | Current enemy slot being processed (X reg) | -| `FrameCounter` | `$09` | Global frame counter | -| `TimerControl` | `$0747` | Nonzero = freeze all movement | -| `BowserHitPoints` | `$0483` | Boss HP (init to 10; 0 = dying, negative = dead) | -| `EnemyFrenzyBuffer` | `$06cb` | Cleared by boss each frame to prevent frenzy spawns | -| `PseudoRandomBitReg` | `$07a7` | PRNG value | -| `PowerUpType` | `$39` | Type of powerup to spawn | - -### Sprite OAM - -| Variable | Addr | Description | -|---|---|---| -| `Sprite_Y_Position` | `$0200` | OAM Y position (indexed by sprite data offset) | -| `Sprite_Tilenumber` | `$0201` | OAM tile number | -| `Sprite_Attributes` | `$0202` | OAM attributes (palette, flip, priority) | -| `Sprite_X_Position` | `$0203` | OAM X position | - -### Sound Queues - -| Variable | Addr | Description | -|---|---|---| -| `Square1SoundQueue` | `$ff` | Sound effect queue channel 1 | -| `Square2SoundQueue` | `$fe` | Sound effect queue channel 2 | -| `AreaMusicQueue` | `$fb` | Music queue | - -### Scratch RAM - -The boss routines use zero-page scratch: `$00`-`$05`, `$eb`. These are assumed temporary and clobbered freely. - -### Other - -| Variable | Addr | Description | -|---|---|---| -| `VerticalFlipFlag` | `$0109` | Used by `DrawSpriteObject` | -| `Alt_SprDataOffset` | `$06ec` | Alternate sprite data offset (for multi-row draws) | -| `pal_type` | `$7e78` | Custom: palette type (set to 3 on boss death) | - ---- - -## 3. Constants - -```asm -; Animation frame indices (into BossGraphics table) -SKIDDING = 3 -JUMPING = 4 -SHOOT = 5*8 ; = 40, byte offset for shoot frame -STANDING = 6 - -; Sound effects -Sfx_Fireball = %00100000 -Sfx_BigJump = %00000001 -Sfx_Blast = %00001000 -Sfx_GrowPowerUp = %00000010 -Silence = %10000000 - -; Music -mus_bossdie = 8 - -; Enemy IDs -BulletBill_FrenzyVar = $08 -BowserFlame = $15 -Bowser = $2d -PowerUpObject = $2e -``` - ---- - -## 4. Graphics Table - -The boss is drawn using player-like CHR tiles. Each animation frame is 8 bytes (4 rows of 2 tile IDs). - -```asm -BossGraphics: -; row0 row1 row2 row3 (each row = 2 tiles: left, right) - .db $00, $01, $02, $03, $04, $05, $06, $07 ; frame 0: walking 1 - .db $08, $09, $0a, $0b, $0c, $0d, $0e, $0f ; frame 1: walking 2 - .db $10, $11, $12, $13, $14, $15, $16, $17 ; frame 2: walking 3 - .db $18, $19, $1a, $1b, $1c, $1d, $1e, $1f ; frame 3: skidding - .db $20, $21, $22, $23, $24, $25, $26, $27 ; frame 4: jumping - .db $08, $09, $28, $29, $2a, $2b, $0e, $0f ; frame 5: fireball throwing (top half reuses walk2) - .db $00, $01, $4c, $4d, $4a, $51, $4b, $52 ; frame 6: standing -``` - ---- - -## 5. Init Routine - -Called when the boss enemy object is first created. Sets HP, direction, and timers. - -```asm -InitBoss: - lda Enemy_X_Position,x - sta BowserOrigXPos ;store original horizontal position - lda #$df - sta BowserFireBreathTimer ;store timer value - lda #2 - sta Enemy_MovingDir,x ;face left - lda #$20 - sta BowserFeetCounter ;set timer - sta EnemyFrameTimer,x - lda #10 - sta BowserHitPoints ;10 hit points - lsr - sta BowserMovementSpeed ;default movement speed = 5 - rts -``` - -> **Note:** `BowserOrigXPos`, `BowserFeetCounter`, `BowserMovementSpeed`, `BowserFireBreathTimer`, and `EnemyFrameTimer` are used by the original Bowser init but are NOT used by the shadow Mario `RunBoss` code. You likely only need to initialize `BowserHitPoints`, `Enemy_MovingDir`, and clear the `boss_*` RAM block to zero. - -### Minimal Init for Porting - -```asm -InitShadowBoss: - ; Clear all boss RAM - lda #0 - ldx #BOSS_RAM_SIZE - @loop: - sta boss_ram_start,x - dex - bpl @loop - sta boss_freeze - - ; Set HP - lda #10 - sta BowserHitPoints - - ; Face left - ldx ObjectOffset - lda #2 - sta Enemy_MovingDir,x - rts -``` - ---- - -## 6. Update Routine (Main Entry Point) - -Called once per frame while the boss is alive. This is the top-level boss tick. - -```asm -RunBoss: - lda boss_freeze - beq @notfreezed - dec boss_freeze - lda #STANDING - sta boss_animation - ; Draw every other frame (flicker) - lda FrameCounter - lsr - bcs @nodraw2 - jsr RelativeEnemyPosition - jsr BossGraphicsHandler - @nodraw2: - ldx ObjectOffset - rts - - @notfreezed: - lda BowserHitPoints - bmi AnimateKilledBoss ; HP < 0: boss is fully dead - jeq KillBoss ; HP == 0: playing death countdown - - lda #80 - sta boss_counter ; reset death countdown each frame while alive - lda #0 - sta EnemyFrenzyBuffer ; suppress enemy frenzy spawns - - ; Draw (skip every other frame if invincible for blink effect) - lda boss_invincibility_timer - beq @draw - lda FrameCounter - lsr - bcs @nodraw - @draw: - jsr RelativeEnemyPosition - jsr BossGraphicsHandler - @nodraw: - ldx ObjectOffset - - lda TimerControl - bne @nograv ; skip movement if timer frozen - - jsr EnemyToBGCollisionDet ; background collision - jsr HandleBossMovement ; AI state machine - - lda Enemy_State,x - and #%01000000 ; check airborne flag - beq @nograv - jsr MoveD_EnemyVertically ; apply gravity - lda #JUMPING - sta boss_animation - @nograv: - - ; Collision detection (only when not invincible) - lda boss_invincibility_timer - bne @decrement_timer - lda #$0a - sta Enemy_BoundBoxCtrl,x ; set bounding box size - jsr GetEnemyOffscreenBits - jsr GetEnemyBoundBox - jmp PlayerEnemyCollision ; check player<->boss collision - - @decrement_timer: - dec boss_invincibility_timer - rts -``` - ---- - -## 7. AI State Machine - -`HandleBossMovement` dispatches based on `boss_state`: - -```asm -HandleBossMovement: - lda boss_state - jsr JumpEngine - .dw MovBossSide ; state 0 - .dw MovBossRunning ; state 1 - .dw MovBossMiddle ; state 2 -``` - -### State 0: Side/Idle (`MovBossSide`) - -Boss stands in place, randomly shoots fireballs and jumps. After `boss_state_timer` expires, transitions to either state 1 (running) or state 2 (middle) based on random + player position. - -```asm -MovBossSide: - lda #STANDING - sta boss_animation - - ; Random fireball: every 8th frame, 1/4 chance - lda FrameCounter - and #%00000111 - bne @no_fireball - lda PseudoRandomBitReg - and #%00000011 - bne @no_fireball - jsr BossSpawnFireball - @no_fireball: - - ; Random jump: every 8th frame (offset 4), 1/2 chance - lda Enemy_State,x - and #%01000000 - bne @no_jump - lda FrameCounter - and #%00000111 - cmp #4 - bne @no_jump - lda PseudoRandomBitReg - lsr - bcc @no_jump - lda #-5 - sta Enemy_Y_Speed,x - jsr BossJump - @no_jump: - - ; State transition when timer expires - lda boss_state_timer - bne @keep_state - - lda #0 - sta boss_substate - lda FrameCounter - clc - adc Player_X_Position - lsr - lsr - bcs @middle - lda #2 ; -> state 2 (middle) - .db $2c ; skip next instruction (BIT abs trick) - @middle: - lda #1 ; -> state 1 (running) - sta boss_state - inc boss_bridge - - @keep_state: - dec boss_state_timer - rts -``` - -### State 1: Running (`MovBossRunning`) - -Boss runs toward the opposite side, accelerating then decelerating/skidding. Optionally jumps if `boss_bridge` is set. - -```asm -MovBossRunning: - jsr BossSetWalkingAnim - - lda Enemy_MovingDir,x - cmp #$01 - beq MovBossRight - - ; Moving left - lda Enemy_X_Position,x - cmp #11*16 - bcs @StillAccer - cmp #4*16 - bcs MovStillMoving - @Deaccelerating: - lda #SKIDDING - sta boss_animation - inc Enemy_X_Speed,x - inc Enemy_X_Speed,x - lda Enemy_X_Speed,x - bmi MovStillMoving - jmp MovChangeState - @StillAccer: - dec Enemy_X_Speed,x - dec Enemy_X_Speed,x - -MovStillMoving: - lda boss_bridge - beq @no_jump - dec boss_bridge - lda Enemy_State,x - and #%01000000 - bne @no_jump - lda #-4 - sta Enemy_Y_Speed,x - jsr BossJump - @no_jump: - jmp MoveEnemyHorizontally - -MovBossRight: - lda Enemy_X_Position,x - cmp #4*16 - bcc @StillAccer - cmp #11*16 - bcc MovStillMoving - @Deaccelerating: - lda #SKIDDING - sta boss_animation - dec Enemy_X_Speed,x - dec Enemy_X_Speed,x - lda Enemy_X_Speed,x - bpl MovStillMoving - jmp MovChangeState - @StillAccer: - inc Enemy_X_Speed,x - inc Enemy_X_Speed,x - jmp MoveEnemyHorizontally - -MovChangeState: - lda PseudoRandomBitReg - and #$f0 - sta boss_state_timer ; random duration for next idle - lda #STANDING - sta boss_animation - lda #0 - sta boss_state ; -> state 0 (side/idle) - lda Enemy_MovingDir,x - eor #%00000011 ; flip direction - sta Enemy_MovingDir,x - rts -``` - -### State 2: Middle (`MovBossMiddle`) - -Boss walks to center of arena, then stands and shoots bullet bills from screen edges. After all enemies are cleared, spawns a powerup if player needs one, then transitions to state 1 (running). - -```asm -MovBossMiddle: - lda boss_substate - beq MovGoToMiddle ; substate 0: walk to center - ; substate 1: middle action (shoot bullets, wait for enemies to die) - - lda #STANDING - sta boss_animation - lda boss_state_timer - bne @not_ready - - ; Timer expired: check if all enemy slots 1-4 are clear - ldy #1 - @loop: - lda Enemy_Flag,y - bne @notyet ; slot still occupied, wait - iny - cpy #5 - bcc @loop - - ; All clear: maybe spawn powerup - lda PlayerStatus - cmp #2 - bcs @no_need_for_p ; player already has fire power - sta PowerUpType - lda #PowerUpObject - cmp Enemy_ID+5 - beq @no_need_for_p ; powerup already exists - ; ... (spawns powerup in slot 5) ... - @no_need_for_p: - lda #0 - sta boss_bridge - lda #1 - sta boss_state ; -> state 1 (running) - ; Set speed based on facing direction - lda Enemy_MovingDir,x - lsr - bcs @right - lda #-$20 - .db $2c - @right: - lda #$20 - sta Enemy_X_Speed,x - @notyet: - rts - - @not_ready: - dec boss_state_timer - ; Every 16 frames, spawn a bullet bill from a random Y position - lda FrameCounter - and #%00001111 - bne @done - ; ... (random Y calculation, alternates left/right side) ... - jsr BossSpawnSideBullet - @done: - rts - -MovGoToMiddle: - ; Walks toward X=$40 (right-facing) or X=$B0 (left-facing) - ; Jumps onto the center platform when reaching the target - ; Sets boss_substate=1 and boss_state_timer when arrived - ; ... (see full source for details) ... -``` - -### Helpers - -```asm -BossSetWalkingAnim: - lda FrameCounter - and #%00000011 - cmp #%00000011 - bne @init_anim - inc boss_walking_anim - lda boss_walking_anim - cmp #$03 - bne @init_anim - lda #0 - sta boss_walking_anim - @init_anim: - lda boss_walking_anim - sta boss_animation - rts - -BossJump: - lda #Sfx_BigJump - sta Square1SoundQueue - lda Enemy_State,x - ora #%01000000 ; set airborne flag - sta Enemy_State,x - rts -``` - ---- - -## 8. Draw Routine - -Draws the boss as a 16x32 pixel sprite (4 rows of 2 tiles). Uses the SMB1 `DrawSpriteObject` routine. - -```asm -BossGraphicsHandler: - lda Enemy_Y_Position,x - sta $02 ; Y position - lda Enemy_Rel_XPos - sta $05 ; screen-relative X - sta VerticalFlipFlag - lda #0 - ldy boss_animation - cpy #SKIDDING - bne + - lda #%00000011 ; flip both axes when skidding - + eor Enemy_MovingDir,x - sta $03 ; horizontal flip control - lda #1 - sta $04 ; sprite palette - - ldy Enemy_SprDataOffset,x - lda boss_shootanim - bne @shootanim - - ; Normal draw: look up animation frame in BossGraphics - lda boss_animation - asl - asl - asl ; * 8 = byte offset - tax - jsr DrawBossRow ; row 0 (head) - jsr DrawBossRow ; row 1 (torso) - ldy Alt_SprDataOffset+1 - jsr DrawBossRow ; row 2 (legs top) - @drawrest: - ldy $06eb - jmp DrawBossRow ; row 3 (legs bottom) - - @shootanim: - ; Shoot: draw shoot frame for top 2 rows, normal for bottom 2 - dec boss_shootanim - ldx #SHOOT ; = 40, offset to shoot frame - jsr DrawBossRow - jsr DrawBossRow - ldy Alt_SprDataOffset+1 - jsr DrawBossRow - lda boss_animation - asl - asl - asl - clc - adc #6 ; skip to bottom 2 tiles of normal frame - tax - jmp @drawrest - -DrawBossRow: - lda BossGraphics,x - sta $00 ; left tile - lda BossGraphics+1,x - sta $01 ; right tile - jmp DrawSpriteObject ; engine routine: writes 2 sprites to OAM -``` - ---- - -## 9. Projectile Spawning - -### Spawn Fireball - -Fires a `BowserFlame` projectile in the boss's facing direction. - -```asm -BossSpawnFireball: - jsr FindID ; find empty enemy slot -> Y - bmi @done ; no slot available - lda #Sfx_Fireball - sta Square1SoundQueue - lda #4 - sta boss_shootanim ; trigger shoot animation - lda Enemy_Y_Position,x - clc - adc #4 - sta $00 ; Y pos - lda Enemy_X_Position,x - sta $01 ; X pos - lda #0 - sta $02 ; page - lda #8 - sta $03 ; bounding box size - lda Enemy_MovingDir,x - cmp #$01 - bne @no - pha - lda $01 - clc - adc #8 ; offset X if facing right - sta $01 - pla - @no: - sta Enemy_MovingDir,y - lda #BowserFlame - jsr BossSpawnEnemy - ldx ObjectOffset - @done: - rts -``` - -### Spawn Side Bullet - -Spawns a `BulletBill_FrenzyVar` from the left or right edge of the screen at a given Y position. - -```asm -; A = direction (1=left edge, 2=right edge) -; $00 = Y position -BossSpawnSideBullet: - sta $02 - jsr FindID - bmi @done - lda $02 - pha - lda #Sfx_Blast - sta Square2SoundQueue - - lda $02 - lsr - bcs @rightdir - - lda #$f0 - sta $01 ; X = left edge ($F0) - lda #-60 - sta bbSpeed,y ; speed = -60 (move right... from left) - jmp @rest - - @rightdir: - lda #0 - sta $01 ; X = right edge ($00) - lda #60 - sta bbSpeed,y ; speed = 60 (move left... from right) - - @rest: - lda #8 - sta $03 ; bounding box - lda #0 - sta $02 ; page - lda #BulletBill_FrenzyVar - sty $eb - jsr BossSpawnEnemy - ldy $eb - pla - sta Enemy_MovingDir,y - ldx ObjectOffset - @done: - rts -``` - -### Helpers - -```asm -; Spawn enemy in slot Y with parameters in $00-$03 -; A = enemy ID -BossSpawnEnemy: - sta Enemy_ID,y - lda $00 - sta Enemy_Y_Position,y - lda $01 - sta Enemy_X_Position,y - lda $02 - sta Enemy_PageLoc,y - lda $03 - sta Enemy_BoundBoxCtrl,y - lda #$01 - sta Enemy_Y_HighPos,y - sta Enemy_Flag,y - lsr - sta Enemy_X_MoveForce,y - sta Enemy_State,y - ldx $04 ; slot index - jmp CheckpointEnemyID ; engine: initialize enemy by ID - -; Find first empty enemy slot (1-5), return in Y. N flag set if none found. -FindID: - ldy #5 - @loop: - lda Enemy_Flag,y - beq @foundSlot - dey - bpl @loop - @foundSlot: - sty $04 - rts -``` - ---- - -## 10. Death Sequence - -### KillBoss (HP == 0 countdown) - -```asm -KillBoss: - lda boss_counter - bmi + ; counter expired -> signal fully dead - lda #Silence - sta AreaMusicQueue - dec boss_counter - - jsr EnemyToBGCollisionDet - lda #STANDING - sta boss_animation - lda Enemy_State,x - and #%01000000 - beq @nograv - jsr MoveD_EnemyVertically - lda #JUMPING - sta boss_animation - @nograv: - ; Draw every other frame (flash effect) - lda FrameCounter - lsr - bcs @nodraw - jsr RelativeEnemyPosition - jsr BossGraphicsHandler - @nodraw: - ldx ObjectOffset - rts - - + ; Counter expired: signal death complete - dec BowserHitPoints ; HP goes to $FF (negative) - lda #mus_bossdie - sta AreaMusicQueue - rts -``` - -### AnimateKilledBoss (HP < 0: spawn powerup and erase) - -```asm -AnimateKilledBoss: - jsr EraseEnemyObject - - lda #3 - sta pal_type ; custom: set palette type for post-boss - lda #2 - sta PowerUpType - lda #PowerUpObject - sta Enemy_ID+5 - lda #0 - sta Enemy_PageLoc+5 - sta Enemy_X_Speed+5 - lda Enemy_X_Position,x - sta Enemy_X_Position+5 ; spawn at boss's position - lda #$01 - sta Enemy_Y_HighPos+5 - sta Enemy_Flag+5 - lda Enemy_Y_Position,x - sta Enemy_Y_Position+5 - lda #$80 - sta Enemy_State+5 - lda #$03 - sta Enemy_BoundBoxCtrl+5 - - lda #-3 - sta Enemy_Y_Speed+5 ; powerup floats upward - - lda #Sfx_GrowPowerUp - sta Square2SoundQueue - ldx ObjectOffset - rts -``` - ---- - -## 11. Damage Handling - -Called when player stomps or fireballs hit the boss. Located separately from the main boss code. - -```asm -StompedBoss: - lda Player_Y_Position - clc - adc #10 - cmp Enemy_Y_Position,x - bcc + - jmp StompInjury ; player below boss -> normal stomp injury to PLAYER - + lda #$fd - sta Player_Y_Speed ; bounce player upward - lda boss_state - bne InjuredBoss ; if not in state 0, just take damage - lda #0 - sta boss_state_timer ; if in idle state, also reset timer - -InjuredBoss: - lda boss_invincibility_timer - bne @done ; already invincible, no damage - inc boss_screech ; track hit count - dec BowserHitPoints ; reduce HP - lda #80 - sta boss_invincibility_timer ; 80 frames of invincibility - @done: - rts -``` - ---- - -## 12. External Dependencies - -Your target project must provide these routines: - -| Routine | Description | -|---|---| -| `EnemyToBGCollisionDet` | Check enemy object vs background tile collision. Updates `Enemy_State` flags. | -| `MoveD_EnemyVertically` | Apply gravity/vertical speed to enemy. Uses `Enemy_Y_Speed`, `Enemy_Y_Position`. | -| `MoveEnemyHorizontally` | Apply horizontal speed to enemy. Uses `Enemy_X_Speed`, `Enemy_X_Position`. | -| `RelativeEnemyPosition` | Calculate screen-relative X/Y from absolute position. Stores to `Enemy_Rel_XPos`/`Enemy_Rel_YPos`. | -| `GetEnemyOffscreenBits` | Determine if enemy is off any screen edge. | -| `GetEnemyBoundBox` | Set up bounding box coordinates from `Enemy_BoundBoxCtrl`. | -| `PlayerEnemyCollision` | Detect and handle player vs enemy collision (calls `StompedBoss` for this enemy). | -| `DrawSpriteObject` | Draw a row of 2 sprites to OAM. Reads tile IDs from `$00`/`$01`, position from `$02`/`$05`, attributes from `$03`/`$04`. Advances Y by 8 for next row. | -| `EraseEnemyObject` | Clear enemy from active slot (zero out `Enemy_Flag`, etc). | -| `JumpEngine` | Indirect jump dispatch: reads A as index, jumps to address from following `.dw` table. | -| `MoveFireballVertically` | Apply gravity to fireball projectiles (used by `RunFireball`, not included here). | -| `OffscreenBoundsCheck` | Despawn enemy if too far offscreen (used by `RunFireball`, not included here). | -| `CheckpointEnemyID` | Initialize a newly spawned enemy by its ID. X = slot index. | -| `StompInjury` | Standard SMB1 stomp injury to player (when stomp fails). | -| `DrawFirebar` | Draw a single firebar/fireball sprite (used by fireball draw, not included here). | -| `ProcFireball` / `RunFireball` | Fireball projectile update logic (not included; you need your own projectile system). | diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index d34883e21..9d024071c 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -549,13 +549,18 @@ NoDecTimers: beq @SkipScrollLock lda Player_X_Position cmp #3 - bcc @Lock + bcc @LockLeft ; Check for right lock cmp #$e5 bcc @SkipScrollLock ; You are still in bounds - @Lock: - .import ImpedePlayerMove - jsr ImpedePlayerMove + lda #$e4 + .byte $2c + @LockLeft: + lda #4 + @StopMovement: + sta Player_X_Position + lda #0 + sta Player_X_Speed @SkipScrollLock: jsr SetPlayerDownstabbingHitbox From 5e861c9c52d3184b83547561a7619b533c43e981 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 9 May 2026 15:30:49 -0400 Subject: [PATCH 023/128] Mostly working boss behavior finally --- RandomizerCore/Asm/z2mario/boss.s | 533 ++++++++++++++++------- RandomizerCore/Asm/z2mario/integration.s | 26 +- RandomizerCore/Asm/z2r.inc | 13 +- 3 files changed, 388 insertions(+), 184 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index 0bc864507..74ea8243f 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -6,6 +6,8 @@ .import DrawMetasprite, METASPRITE_BIG_MARIO_WALKING_1 .import SwapPRG, SwapToSavedPRG, SwapToPRG0 .import METASPRITE_SMALL_MARIO_DEATH +.import METASPRITE_FIREBALL_FRAME_1, METASPRITE_FIREBALL_FRAME_2 +.import METASPRITE_HAMMER_FRAME_1, METASPRITE_HAMMER_FRAME_2 .export InjuredBoss @@ -18,8 +20,21 @@ STANDING = METASPRITE_BIG_MARIO_STANDING WALKING = METASPRITE_BIG_MARIO_WALKING_1 mus_bossdie = 8 -; Boss RAM size for clearing loop -;BOSS_RAM_SIZE = 11 ; boss_animation through boss_freeze (12 bytes, 0-indexed = 11) +.segment "PRG7" +; patch the vanilla enemy projectile check to prevent it from trying to run our hammer/fireball +.org $d57f + jsr DontRunHammerFireball +.reloc +DontRunHammerFireball: + cmp #METASPRITE_FIREBALL_FRAME_1 + bcs @HammerFireball + sec + sbc #1 + rts +@HammerFireball: + pla + pla + rts .segment "PRG5", "PRG7" @@ -49,7 +64,8 @@ BossLandedInCutscene: .org $988e jsr RunBoss jmp $9a77 -FREE_UNTIL $9a77 +FREE_UNTIL $9a0f + ; Patch bank5_Enemy_Vulnerability_Damage_Codes entry $23 (Dark Link): ; Clear bit 5 "Immune to Flying Blade and Fire" so fireballs can hit the boss. @@ -181,6 +197,8 @@ RunBoss: jsr EnemyToBGCollisionDet ; background collision jsr HandleBossMovement ; AI state machine + jsr ClampBossX ; keep boss inside arena [16, 236] + jsr BossUpdateProjectiles ; move shadow fireballs/hammers + collision lda Enemy_State,x and #%01000000 ; check airborne flag @@ -222,6 +240,7 @@ MovBossSide: lda RNG and #%00000011 bne @no_fireball + jsr BossFacePlayer ; aim at Mario before shooting jsr BossSpawnFireball @no_fireball: @@ -355,34 +374,6 @@ MovBossMiddle: cpy #5 bcc @loop - ; All clear: maybe spawn powerup -; lda Player_State -; cmp #2 -; bcs @no_need_for_p ; player already has fire power -; sta PowerUpType -; lda #PowerUpObject -; cmp Enemy_ID+5 -; beq @no_need_for_p ; powerup already exists -; ; Spawn powerup in slot 5 -; ldy #5 -; lda #PowerUpObject -; sta Enemy_ID,y -; lda Enemy_X_Position,x -; sta Enemy_X_Position+5 -; lda Enemy_Y_Position,x -; sta Enemy_Y_Position+5 -; lda #0 -; sta Enemy_PageLoc+5 -; sta Enemy_X_Speed+5 -; lda #$01 -; sta Enemy_Y_HighPos+5 -; sta Enemy_Flag+5 -; lda #$80 -; sta Enemy_State+5 -; lda #$03 -; sta Enemy_BoundBoxCtrl+5 -; lda #<(-3) -; sta Enemy_Y_Speed+5 @no_need_for_p: lda #0 sta boss_bridge @@ -402,66 +393,58 @@ MovBossMiddle: @not_ready: dec boss_state_timer - ; Every 16 frames, spawn a bullet bill from a random Y position + ; Every 24 frames, lob a shadow hammer from the boss with a random + ; X-speed; BossSpawnHammer biases the throw toward Mario. lda FrameCounter - and #%00001111 + and #%00010111 bne @done - ; Random Y position - lda RNG - and #$70 - clc - adc #$30 - sta $00 ; Y position for bullet - ; Alternate left/right side - lda FrameCounter - and #%00010000 - lsr - lsr - lsr - lsr - clc - adc #1 ; A = 1 or 2 (left or right) - jsr BossSpawnSideBullet + jsr BossSpawnHammer @done: rts +; Right-side platform target: in vanilla Zelda 2, the great-palace dark-link +; arena has a 3-metatile-wide platform on the right, 7 metatiles above the +; ground. Center of platform is X = $D8. The boss walks toward $D8 from +; whichever side it's on, then jumps up to land on top. +PLATFORM_TARGET_X = $D8 +PLATFORM_TOLERANCE = 4 ; arrive band: target +/- 4 pixels + MovGoToMiddle: jsr BossSetWalkingAnim - ; Target X: $B0 if facing left, $40 if facing right - lda Enemy_MovingDir,x - lsr - bcs @facing_right - ; Facing left: target $B0 + ; Walk toward PLATFORM_TARGET_X. Boss is "arrived" once within +/-4 px. lda Enemy_X_Position,x - cmp #$B0 - bcc @keep_walking - bcs @arrived - @facing_right: - ; Facing right: target $40 - lda Enemy_X_Position,x - cmp #$80 - bcs @arrived - @keep_walking: - ; Set walk speed based on direction - lda Enemy_MovingDir,x - lsr - bcs @walk_right - lda #<(-10) - .byte $2c ; skip next instruction + cmp #PLATFORM_TARGET_X - PLATFORM_TOLERANCE + bcc @walk_right + cmp #PLATFORM_TARGET_X + PLATFORM_TOLERANCE + bcs @walk_left + jmp @arrived + @walk_right: - lda #10 + lda #1 + sta Enemy_MovingDir,x + sta EnemyFacingDir,x + lda #20 + sta Enemy_X_Speed,x + jmp MoveEnemyHorizontally + + @walk_left: + lda #2 + sta Enemy_MovingDir,x + sta EnemyFacingDir,x + lda #<(-20) sta Enemy_X_Speed,x jmp MoveEnemyHorizontally @arrived: lda #0 sta Enemy_X_Speed,x - ; Jump onto center platform + ; Jump onto right platform. + ; lift under MoveD_EnemyVertically's gravity to clear ~7 metatiles. lda Enemy_State,x and #%01000000 bne @already_airborne - lda #<(-6) + lda #<(-8) sta Enemy_Y_Speed,x jsr BossJump @already_airborne: @@ -488,6 +471,47 @@ BossSetWalkingAnim: sta boss_animation rts +.reloc +; Face the player. Sets Enemy_MovingDir to 1 (right) if the player is to the +; right of the boss, otherwise 2 (left). Z2 enemy MovingDir conventions: +; 1 = right, 2 = left. +BossFacePlayer: + ldx #0 + lda Player_X_Position + cmp Enemy_X_Position,x + lda #1 ; default: face right + bcs @store + lda #2 ; player is left of boss: face left + @store: + sta Enemy_MovingDir,x + sta EnemyFacingDir,x + rts + +.reloc +; Keep the boss inside the arena bounds [16, 236]. If Enemy_X_Speed accelerates +; far enough to drive Enemy_X_Position past either wall (including unsigned +; underflow when running left) we hard-clamp the position and zero the speed so +; MovBossRunning's deceleration logic can take over normally. +ClampBossX: + ldx #0 + lda Enemy_X_Position,x + cmp #16 + bcs @notLeft + lda #16 + sta Enemy_X_Position,x + lda #0 + sta Enemy_X_Speed,x + beq @done + @notLeft: + cmp #237 + bcc @done + lda #236 + sta Enemy_X_Position,x + lda #0 + sta Enemy_X_Speed,x + @done: + rts + .reloc BossJump: lda #Sfx_BigJump @@ -499,129 +523,316 @@ BossJump: .segment "PRG7" .reloc +; Draw the shadow-mario boss (and, eventually, its projectiles) into OAM. +; Hooked at $9a7f, which is part of the bank5 dark-link state machine and +; runs *independently* of PatchLinkDrawRoutine, so the boss no longer +; flickers in lockstep with Mario's hurt/invincibility flicker. +; +; DrawMetasprite lives in PRG0, so we save the current bank, swap in PRG0, +; do the draw, then restore the bank that was loaded at $8000/$A000 on +; entry (typically bank5, since we were called from bank5 code). +; +; Boss sprites are written starting at OAM byte 48 - past the slots that +; PatchLinkDrawRoutine uses for the player + shield + Mario projectiles - +; so neither call clobbers the other regardless of which runs first. +BOSS_OAM_OFFSET = 48 + BossGraphicsHandler: + lda boss_animation + bne @active + rts +@active: + + lda NmiBankShadow8 + pha + lda NmiBankShadowA + pha + + lda #0 + jsr SwapPRG + ; Boss-owned flicker: when invincible, draw on alternating frames only. + ; This is decoupled from any player/injury flicker. + lda boss_invincibility_timer + beq @draw + lda FrameCounter + lsr + bcs @restore +@draw: + lda #BOSS_OAM_OFFSET + sta CurrentOAMOffset + ldy #1 ; sprite slot 1 (Y for DrawMetasprite & SprAttrib,y) + lda #2 ; palette 3 attribute (shadow mario palette) + sta SprObject_SprAttrib,y + ldx boss_animation + jsr DrawMetasprite + jsr BossDrawProjectiles ; shadow fireballs + hammers (palette already set per-slot) + +@restore: + pla + sta NmiBankShadowA + sta PrgBankAReg + pla + sta NmiBankShadow8 + sta PrgBank8Reg rts .segment "PRG5", "PRG7" + +; --- Boss projectile system --------------------------------------------------- +; Boss projectiles live in SprObject indices 7 and 8 - the slots between the 6 +; vanilla enemy slots and Mario's fireball/hammer pair. DrawMetasprite reads +; SprObject_*,y directly so once a slot is populated we can render it with the +; existing METASPRITE_FIREBALL_FRAME_x / METASPRITE_HAMMER_FRAME_x metasprites. +; +; A slot is active when EnemyProjectileType,n != 0 (zero = inactive). We never +; leave a metasprite ID stored at $87..$88 outside of our own update/draw paths +; so vanilla projectile dispatch can't mistake it for a Z2 projectile type. +; +; We co-opt one bit of SprObject_SprAttrib,n to remember the type: +; bit 6 = 1 -> hammer (lobbed, gravity) +; bit 6 = 0 -> fireball (straight horizontal) +; Bits 0-1 stay set to %10 = palette 3 (shadow palette) for both types. + +PROJ_ATTR_FIREBALL = %00000010 ; palette 3 +PROJ_ATTR_HAMMER = %01000010 ; palette 3 + "is hammer" flag (uses OAM bit 6, harmless on render) + .reloc -FindID: - ; Find first empty enemy slot (5 down to 1), return in Y. - ; N flag set if none found. - ldy #5 +; FindFreeBossProjSlot: returns Y = 7..(7+ENEMY_PROJECTILE_COUNT-1) for an empty slot, +; or Y < 7 with N flag set if none available. Active = EnemyProjectileType,Y != 0. +FindFreeBossProjSlot: + ldy #EnemyProjectileOffset @loop: - lda Enemy_Flag,y - beq @foundSlot - dey - bne @loop ; stop at 0 (don't use boss slot) - dey ; Y = $FF, N flag set - @foundSlot: - sty $04 + lda EnemyProjectileType - EnemyProjectileOffset,y + beq @found + iny + cpy #EnemyProjectileOffset + ENEMY_PROJECTILE_COUNT + bcc @loop + ldy #0 + dey ; Y = $FF, N set, BCC won't take + @found: rts .reloc +; BossSpawnFireball: spawn a horizontal shadow fireball from the boss aimed at +; Mario. X must hold the boss enemy slot (= 0). BossSpawnFireball: - jsr FindID ; find empty enemy slot -> Y - bmi @done ; no slot available + jsr FindFreeBossProjSlot + bmi @done lda #Sfx_Fireball sta Square1SoundQueue lda #4 - sta boss_shootanim ; trigger shoot animation + sta boss_shootanim ; trigger shoot animation pose + + ; Position: emit from boss center lda Enemy_Y_Position,x clc adc #4 - sta $00 ; Y pos + sta SprObject_Y_Position,y lda Enemy_X_Position,x - sta $01 ; X pos - lda #0 - sta $02 ; page - lda #8 - sta $03 ; bounding box size + sta SprObject_X_Position,y + lda Enemy_PageLoc,x + sta SprObject_PageLoc,y + lda #1 + sta SprObject_Y_HighPos,y + + ; Direction follows boss facing (already aimed by BossFacePlayer) lda Enemy_MovingDir,x - cmp #$01 - bne @no - pha - lda $01 - clc - adc #8 ; offset X if facing right - sta $01 - pla - @no: - sta Enemy_MovingDir,y - lda #5 ; TODO What to spawn here? BowserFlame - jsr BossSpawnEnemy + sta Player_MovingDir,y ; DrawMetasprite uses MovingDir for non-mario sprites + cmp #1 + beq @right + lda #<(-3) ; left + .byte $2c + @right: + lda #3 ; right + sta SprObject_X_Speed,y + lda #0 + sta SprObject_Y_Speed,y ; fireballs travel level + + lda #PROJ_ATTR_FIREBALL + sta SprObject_SprAttrib,y + lda #METASPRITE_FIREBALL_FRAME_1 + sta EnemyProjectileType - EnemyProjectileOffset,y + ldx #0 @done: rts .reloc -BossSpawnSideBullet: - ; A = direction (1=left edge, 2=right edge) - ; $00 = Y position (already set by caller) - sta $02 - jsr FindID +; BossSpawnHammer: lob a hammer from the boss with a randomised X-speed so +; successive hammers leave gaps along the floor where Mario can dodge. Sign +; of the X-speed is biased toward Mario. Initial Y-speed is upward; gravity +; in BossUpdateProjectiles brings it back down in an arc. +BossSpawnHammer: + jsr FindFreeBossProjSlot bmi @done - lda $02 - pha - lda #Sfx_Blast - sta Square2SoundQueue + lda #Sfx_Fireball + sta Square1SoundQueue - lda $02 - lsr - bcs @rightdir + ; Spawn just above the boss + lda Enemy_Y_Position,x + sec + sbc #8 + sta SprObject_Y_Position,y + lda Enemy_X_Position,x + sta SprObject_X_Position,y + lda Enemy_PageLoc,x + sta SprObject_PageLoc,y + lda #1 + sta SprObject_Y_HighPos,y - lda #$f0 - sta $01 ; X = left edge ($F0) - lda #<(-60) - sta ProjectileXVelocity,y ; speed = -60 (move right from left) - jmp @rest + ; Random X-speed magnitude in 1..4, sign points toward Mario. + lda RNG + and #$03 + clc + adc #1 + pha + lda Player_X_Position + cmp Enemy_X_Position,x + bcs @aim_right + pla + eor #$ff + clc + ; negate (toward left) + adc #1 + jmp @set_speed + @aim_right: + pla + @set_speed: + sta SprObject_X_Speed,y - @rightdir: - lda #0 - sta $01 ; X = right edge ($00) - lda #60 - sta ProjectileXVelocity,y ; speed = 60 (move left from right) + ; initial upward velocity + lda #<(-6) + sta SprObject_Y_Speed,y - @rest: - lda #8 - sta $03 ; bounding box - lda #0 - sta $02 ; page + lda Enemy_MovingDir,x + sta Player_MovingDir,y + lda #PROJ_ATTR_HAMMER + sta SprObject_SprAttrib,y + lda #METASPRITE_HAMMER_FRAME_1 + sta EnemyProjectileType - EnemyProjectileOffset,y - tya - pha - lda #10 ; TODO: what to use to make a bubble? - jsr BossSpawnEnemy - pla - tay - pla - sta Enemy_MovingDir,y ldx #0 @done: rts .reloc -BossSpawnEnemy: - ; Spawn enemy in slot Y with parameters in $00-$03 - ; A = enemy ID -; sta Enemy_ID,y - lda $00 - sta Enemy_Y_Position,y - lda $01 - sta Enemy_X_Position,y - lda $02 - sta Enemy_PageLoc,y -; lda $03 -; sta Enemy_BoundBoxCtrl,y - lda #$01 - sta Enemy_Y_HighPos,y -; sta Enemy_Flag,y - lsr - sta Enemy_X_MoveForce,y - sta Enemy_State,y +; BossUpdateProjectiles: per-frame movement + offscreen kill + collision check +; for each active boss projectile. Hammers gain +1 to Y velocity each frame +; (gravity); fireballs ignore Y velocity. +BossUpdateProjectiles: + ldy #EnemyProjectileOffset + @loop: + lda EnemyProjectileType - EnemyProjectileOffset,y + bne @active + jmp @next + @active: + ; X position += X speed + clc + lda SprObject_X_Position,y + adc SprObject_X_Speed,y + sta SprObject_X_Position,y + + ; Hammer arc: integrate Y speed and apply gravity + lda SprObject_SprAttrib,y + and #%01000000 ; PROJ_ATTR_HAMMER bit + beq @move_done + clc + lda SprObject_Y_Position,y + adc SprObject_Y_Speed,y + sta SprObject_Y_Position,y + ; Apply gravity once every 2 frames so hammers don't sink too fast + lda FrameCounter + lsr + bcs @move_done + lda SprObject_Y_Speed,y + adc #1 + sta SprObject_Y_Speed,y + @move_done: + + ; Offscreen kill: remove if X out of [0, 248] or Y >= $D8 (below floor). + lda SprObject_X_Position,y + cmp #248 + bcs @kill + lda SprObject_Y_Position,y + cmp #$D8 + bcs @kill + + ; Collision with player. Boss projectile box: 8x8 at (X,Y). Reuse the + ; vanilla player hitbox loader for $00-$03, and hand-build $04-$07. + lda SprObject_X_Position,y + sta $04 + lda #8 + sta $06 + lda SprObject_Y_Position,y + sta $05 + lda #8 + sta $07 + sty $11 ; preserve loop slot + jsr bank7_LoadLinkHitbox ; Mario body in $00-$03 + jsr bank7_CollisionTest + ldy $11 + bcc @next + ; Hit Mario - flag him for damage and drop projectile + ldx #0 + lda $a8,x + ora #$10 + sta $a8,x + jsr bank7_LinkCollision + + @kill: + lda #0 + sta EnemyProjectileType - EnemyProjectileOffset,y + @next: + iny + cpy #EnemyProjectileOffset + ENEMY_PROJECTILE_COUNT + bcc @loop + ldx #0 rts -; ldx $04 ; slot index -; jmp CheckpointEnemyID ; engine: initialize enemy by ID +.segment "PRG7" +.reloc +; BossDrawProjectiles: invoked from BossGraphicsHandler with PRG0 already paged +; in, so DrawMetasprite is reachable. This routine MUST live in PRG7 (fixed +; bank) so it stays callable after BossGraphicsHandler's SwapPRG(0) replaces +; PRG5 at $8000. Animation frame toggles every 4-8 frames. +BossDrawProjectiles: + ldy #EnemyProjectileOffset + @loop: + lda EnemyProjectileType - EnemyProjectileOffset,y + beq @next + ; Reload animation frame (frame_1 / frame_2) based on FrameCounter. + lda SprObject_SprAttrib,y + and #%01000000 + bne @hammer_anim + lda FrameCounter + and #%00000100 + beq @set_fb1 + lda #METASPRITE_FIREBALL_FRAME_2 + .byte $2c + @set_fb1: + lda #METASPRITE_FIREBALL_FRAME_1 + jmp @do_draw + @hammer_anim: + lda FrameCounter + and #%00001000 + beq @set_h1 + lda #METASPRITE_HAMMER_FRAME_2 + .byte $2c + @set_h1: + lda #METASPRITE_HAMMER_FRAME_1 + @do_draw: + sta EnemyProjectileType - EnemyProjectileOffset,y + tax + sty $11 + jsr DrawMetasprite + ldy $11 + @next: + iny + cpy #EnemyProjectileOffset + ENEMY_PROJECTILE_COUNT + bcc @loop + rts + +.segment "PRG5", "PRG7" .reloc KillBoss: lda boss_counter @@ -632,10 +843,10 @@ KillBoss: lda #CHR_SMALLMARIO sta boss_ChrBank inc ReloadCHRBank - lda #<(-6) ; fast upward launch + lda #<(-6) sta Enemy_Y_Speed,x lda #0 - sta Enemy_X_Speed,x ; stop horizontal movement + sta Enemy_X_Speed,x lda #$7f sta boss_counter ; vanilla boss kill behavior diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 9d024071c..69f49c8cb 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -217,22 +217,6 @@ PatchLinkDrawRoutine: inc ReloadCHRBank + @skipplayer: - ; Check if we are rendering the Mario Boss - lda boss_invincibility_timer - beq + - ; if the boss is flickering skip drawing every other frame - lda FrameCounter - lsr - bcs @noboss - + - ldx boss_animation - beq @noboss - ldy #1 - lda #2 - sta SprObject_SprAttrib,y - jsr DrawMetasprite - ; TODO draw boss spawned objects here -@noboss: ; While we are here, lets draw the fireball/hammer sprites too ldy #ProjectileOffset sty $02 @@ -651,18 +635,16 @@ PatchFireballHitcheck: lda $a1,x cmp #$23 bne @NotBoss - lda $0b - beq @exit ; hammer doesn't damage boss jsr bank7_LoadObjectHitbox ; load boss hitbox ZP $04-$07 - jsr bank7_CollisionTest ; test fireball ($00-$03) vs boss ($04-$07) + jsr bank7_CollisionTest ; test projectile ($00-$03) vs boss ($04-$07) bcc @exit ; miss + jsr InjuredBoss ; decrement HP via controlled path, start invincibility ldy $11 lda Fireball_State,y and #%01000000 - bne @exit ; hammer safety - lda #%10000000 + bne @exit ; hammer passes through; don't despawn it + lda #%10000000 ; fireball: trigger explosion state sta Fireball_State,y - jsr InjuredBoss ; decrement HP via controlled path, start invincibility @exit: rts @NotBoss: diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index f1da677c7..f4efaba24 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -305,7 +305,17 @@ RESV boss_counter RESV boss_freeze RESV boss_ChrBank BOSS_RAM_SIZE = RES_OFFSET - CUSTOM_BOSS_START - 1 -;RESV BowserHitPoints + +EnemyProjectileOffset = 7 +EnemyProjectileType = ObjectMetasprite + EnemyProjectileOffset +EnemyProjectile_X_Position = SprObject_X_Position + EnemyProjectileOffset +EnemyProjectile_Y_Position = SprObject_Y_Position + EnemyProjectileOffset +EnemyProjectile_PageLoc = SprObject_PageLoc + EnemyProjectileOffset +EnemyProjectile_Y_HighPos = SprObject_Y_HighPos + EnemyProjectileOffset +EnemyProjectile_X_Speed = SprObject_X_Speed + EnemyProjectileOffset +EnemyProjectile_Y_Speed = SprObject_Y_Speed + EnemyProjectileOffset +EnemyProjectile_SprAttrib = SprObject_SprAttrib + EnemyProjectileOffset +ENEMY_PROJECTILE_COUNT = 6 ; MARIO SPECIFIC RAM LOCATIONS ; These are values that are hardcoded to match where Zelda 2 places them @@ -363,6 +373,7 @@ RESV CrouchingFlag Player_MovingDir = $5F Fireball_MovingDir = $6D PlayerFacingDir = $9F +EnemyFacingDir = $A0 SavedJoypadBits = $F7 ScreenLeft_PageLoc = $72a ScreenRight_PageLoc = $72b From 9c29ef1825f3414d0cd2afd9546d096847d2d141 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sun, 10 May 2026 17:40:52 -0400 Subject: [PATCH 024/128] Finally. The boss is finally mostly working --- RandomizerCore/Asm/z2mario/boss.s | 502 +++++++++++++++++------- RandomizerCore/Asm/z2mario/metasprite.s | 1 + RandomizerCore/Asm/z2r.inc | 1 + 3 files changed, 353 insertions(+), 151 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index 74ea8243f..ebdcfe25a 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -20,6 +20,31 @@ STANDING = METASPRITE_BIG_MARIO_STANDING WALKING = METASPRITE_BIG_MARIO_WALKING_1 mus_bossdie = 8 + +; Boss settings: + +; Frames the boss spends standing still after each top-level state change. +STATE_SWITCH_DELAY = 50 +; Initial speed kicked in when MovBossRunning starts from a dead stop (e.g. +; after a transition delay zeroed X_Speed, or after dropping off the platform +; into the middle of the arena). +RUNNING_KICK_SPEED = $20 + +; Right-side platform target: the great-palace dark-link arena has a 3-metatile-wide +; platform on the right, 7 metatiles above the ground. Boss will jump up here for a hammer attack +PLATFORM_TARGET_X = $D8 +PLATFORM_TOLERANCE = 4 +PLATFORM_JUMP_DISTANCE = $20 ; trigger arcing leap when this close to target X +JUMP_X_SPEED = 6 ; horizontal speed retained during the jump +JUMP_Y_SPEED = <(-8) ; initial upward velocity (matches old straight-up jump height) + +; Y position above which the boss is considered "on the platform" (the visual +; floor of the arena sits at $A0+, the platform sits 7 metatiles higher). +PLATFORM_TOP_Y_THRESHOLD = $80 + +LEFT_EDGE_TARGET = 6*16 +RIGHT_EDGE_TARGET = 9*16 + .segment "PRG7" ; patch the vanilla enemy projectile check to prevent it from trying to run our hammer/fireball .org $d57f @@ -42,12 +67,13 @@ DontRunHammerFireball: ; as an init routine .org $985d jsr PatchBossInit +; lda #$21 ; make the boss jump further out to make him land past $b0 .reloc PatchBossInit: sta EnemyYVelocity jmp InitShadowBoss -; Patch the location where it draws the boss (effectively a nop) +; Patch the location where it draws the boss .org $9a7f jsr BossGraphicsHandler @@ -58,6 +84,10 @@ BossLandedInCutscene: sta $0505 lda #CROUCHING sta boss_animation + lda #$9F + sta $2a ; force the boss to land at the right height on the ground + lda #2 + sta Enemy_MovingDir ; force the boss to start by facing left rts .segment "PRG5" @@ -192,13 +222,14 @@ RunBoss: @nodraw: ldx #0 + jsr BossUpdateProjectiles + lda TimerControl bne @nograv ; skip movement if timer frozen jsr EnemyToBGCollisionDet ; background collision jsr HandleBossMovement ; AI state machine jsr ClampBossX ; keep boss inside arena [16, 236] - jsr BossUpdateProjectiles ; move shadow fireballs/hammers + collision lda Enemy_State,x and #%01000000 ; check airborne flag @@ -208,12 +239,20 @@ RunBoss: sta boss_animation @nograv: - ; Collision detection (only when not invincible) + ; Collision detection (only when not invincible). While invincible: + ; 1. Skip BossPlayerCollision entirely so the boss body cannot register + ; either a stomp re-hit or contact damage on Mario. + ; 2. Force-clear bit 4 of $A8,x (the enemy-contacted-link flag) so any + ; contact that registered right before the boss took damage can't + ; fire bank7_LinkCollision later in the frame. lda boss_invincibility_timer bne @decrement_timer jmp BossPlayerCollision @decrement_timer: + lda $a8,x + and #%11101111 + sta $a8,x dec boss_invincibility_timer rts AnimateKilledBoss: @@ -222,6 +261,44 @@ AnimateKilledBoss: .reloc HandleBossMovement: + ; If the boss is invincible (just got stomped), force-clear any pending + ; transition delay. When a stomp sets boss_state_timer = 0, the next + ; frame's MovBossSide transition re-arms STATE_SWITCH_DELAY - clearing + ; it here every invincibility frame guarantees the boss skips the + ; standing pause when an attack is cancelled by a hit. + lda boss_invincibility_timer + beq + + lda #0 + sta boss_transition_delay ++ + + ; If a transition delay is in flight, the boss just left an attack and + ; is winding down before the next state runs. Two cases: + ; - Airborne (e.g. mid-leap off the platform): freeze the timer and + ; keep applying X velocity so the jump arc completes. The timer + ; only ticks once we've landed; otherwise zeroing X_Speed strands + ; him over the same column and he never clears the platform edge. + ; - Grounded: stand still, zero X speed, tick the timer down. + lda boss_transition_delay + beq @run_state + lda Enemy_State,x + and #%01000000 + bne @airborne_delay + ; On the ground: standing pause. + dec boss_transition_delay + lda #STANDING + sta boss_animation + lda #0 + sta Enemy_X_Speed,x + rts + @airborne_delay: + ; In flight: hold the timer, finish the arc. X velocity was set by + ; the post-attack transition; gravity is applied in RunBoss after + ; HandleBossMovement returns. + lda #JUMPING + sta boss_animation + jmp MoveEnemyHorizontally +@run_state: lda boss_state jsr JumpEngine .word MovBossSide ; state 0 @@ -278,24 +355,44 @@ MovBossSide: lda #1 ; -> state 1 (running) sta boss_state inc boss_bridge + lda #STATE_SWITCH_DELAY + sta boss_transition_delay @keep_state: dec boss_state_timer rts .reloc + MovBossRunning: jsr BossSetWalkingAnim + lda Enemy_X_Speed,x + bne @speed_ok + lda Enemy_X_Position,x + cmp #LEFT_EDGE_TARGET + bcc @speed_ok ; left wall: leave to @Deaccelerating + cmp #RIGHT_EDGE_TARGET + bcs @speed_ok ; right wall: leave to @Deaccelerating + lda Enemy_MovingDir,x + cmp #1 + beq @kick_right + lda #<(-RUNNING_KICK_SPEED) + .byte $2c + @kick_right: + lda #RUNNING_KICK_SPEED + sta Enemy_X_Speed,x +@speed_ok: + lda Enemy_MovingDir,x cmp #$01 beq MovBossRight ; Moving left lda Enemy_X_Position,x - cmp #11*16 + cmp #RIGHT_EDGE_TARGET bcs @StillAccer - cmp #4*16 + cmp #LEFT_EDGE_TARGET bcs MovStillMoving @Deaccelerating: lda #SKIDDING @@ -312,9 +409,9 @@ MovBossRunning: MovBossRight: lda Enemy_X_Position,x - cmp #4*16 + cmp #LEFT_EDGE_TARGET bcc @StillAccer - cmp #11*16 + cmp #RIGHT_EDGE_TARGET bcc MovStillMoving @Deaccelerating: lda #SKIDDING @@ -342,23 +439,22 @@ MovStillMoving: jmp MoveEnemyHorizontally MovChangeState: - lda RNG - and #$f0 - sta boss_state_timer ; random duration for next idle + ; Running -> idle. + lda #$b0 + sta boss_state_timer lda #STANDING sta boss_animation lda #0 - sta boss_state ; -> state 0 (side/idle) + sta boss_state lda Enemy_MovingDir,x - eor #%00000011 ; flip direction + eor #%00000011 sta Enemy_MovingDir,x rts .reloc MovBossMiddle: lda boss_substate - beq MovGoToMiddle ; substate 0: walk to center - ; substate 1: middle action (shoot bullets, wait for enemies to die) + beq MovGoToMiddle lda #STANDING sta boss_animation @@ -375,24 +471,43 @@ MovBossMiddle: bcc @loop @no_need_for_p: + ; Face Mario before launching off the platform. + jsr BossFacePlayer lda #0 sta boss_bridge - lda #1 - sta boss_state ; -> state 1 (running) - ; Set speed based on facing direction + + ; Set running speed in the (now player-facing) direction. lda Enemy_MovingDir,x lsr bcs @right lda #<(-$20) - .byte $2c ; skip next instruction (BIT abs trick) + .byte $2c @right: lda #$20 sta Enemy_X_Speed,x + + ; Step off the platform: nudge upward and mark airborne so RunBoss's + ; gravity path engages. Without this, the boss might not make it off the platform + ; and then start the run phase while at the top of the screen + lda #<(-3) + sta Enemy_Y_Speed,x + jsr BossJump + + lda #1 + sta boss_state @notyet: rts @not_ready: dec boss_state_timer + + lda Enemy_State,x + and #%01000000 + beq @on_platform + jsr MoveEnemyHorizontally + ldx #0 + @on_platform: + ; Every 24 frames, lob a shadow hammer from the boss with a random ; X-speed; BossSpawnHammer biases the throw toward Mario. lda FrameCounter @@ -402,57 +517,91 @@ MovBossMiddle: @done: rts -; Right-side platform target: in vanilla Zelda 2, the great-palace dark-link -; arena has a 3-metatile-wide platform on the right, 7 metatiles above the -; ground. Center of platform is X = $D8. The boss walks toward $D8 from -; whichever side it's on, then jumps up to land on top. -PLATFORM_TARGET_X = $D8 -PLATFORM_TOLERANCE = 4 ; arrive band: target +/- 4 pixels - MovGoToMiddle: jsr BossSetWalkingAnim - ; Walk toward PLATFORM_TARGET_X. Boss is "arrived" once within +/-4 px. + ; If we're already airborne, the leap is in progress: keep the arc and + ; let gravity + EnemyToBGCollisionDet handle landing on the platform. + lda Enemy_State,x + and #%01000000 + bne @keep_arc + + ; don't start throwing hammers until we are actually landed on the platform + lda Enemy_Y_Position,x + cmp #PLATFORM_TOP_Y_THRESHOLD + bcs @on_lower_floor + ; Standing on the platform - kick off the hammer phase. + lda #0 + sta Enemy_X_Speed,x + lda #1 + sta boss_substate + lda #$80 + sta boss_state_timer + rts + +@on_lower_floor: + ; On ground: figure out which side of the target we're on and how far. lda Enemy_X_Position,x - cmp #PLATFORM_TARGET_X - PLATFORM_TOLERANCE - bcc @walk_right - cmp #PLATFORM_TARGET_X + PLATFORM_TOLERANCE - bcs @walk_left - jmp @arrived + cmp #PLATFORM_TARGET_X + bcs @right_of_target - @walk_right: + ; Boss is left of target. Distance = TARGET - X. + lda #PLATFORM_TARGET_X + sec + sbc Enemy_X_Position,x + sta $00 lda #1 sta Enemy_MovingDir,x sta EnemyFacingDir,x - lda #20 + lda $00 + cmp #PLATFORM_JUMP_DISTANCE + bcs @walk_right_full + lda #JUMP_X_SPEED sta Enemy_X_Speed,x - jmp MoveEnemyHorizontally + jmp @start_jump - @walk_left: +@right_of_target: + lda Enemy_X_Position,x + sec + sbc #PLATFORM_TARGET_X + sta $00 lda #2 sta Enemy_MovingDir,x sta EnemyFacingDir,x - lda #<(-20) + lda $00 + cmp #PLATFORM_TOLERANCE + bcc @on_target_jump_up ; already centered horizontally - jump straight up + cmp #PLATFORM_JUMP_DISTANCE + bcs @walk_left_full + lda #<(-JUMP_X_SPEED) sta Enemy_X_Speed,x - jmp MoveEnemyHorizontally + jmp @start_jump - @arrived: +@on_target_jump_up: lda #0 sta Enemy_X_Speed,x - ; Jump onto right platform. - ; lift under MoveD_EnemyVertically's gravity to clear ~7 metatiles. - lda Enemy_State,x - and #%01000000 - bne @already_airborne - lda #<(-8) + jmp @start_jump + +@walk_right_full: + lda #20 + sta Enemy_X_Speed,x + jmp MoveEnemyHorizontally + +@walk_left_full: + lda #<(-20) + sta Enemy_X_Speed,x + jmp MoveEnemyHorizontally + +@start_jump: + lda #JUMP_Y_SPEED sta Enemy_Y_Speed,x - jsr BossJump - @already_airborne: - lda #1 - sta boss_substate ; switch to substate 1 - lda #$80 - sta boss_state_timer ; timer for bullet phase - rts + jsr BossJump ; sets airborne flag + ; Stay in substate 0 throughout the arc. The landing-detection at the + ; top of MovGoToMiddle flips us to substate 1 once we touch down on + ; the platform; until then no hammers fire. + +@keep_arc: + jmp MoveEnemyHorizontally .reloc BossSetWalkingAnim: @@ -479,9 +628,9 @@ BossFacePlayer: ldx #0 lda Player_X_Position cmp Enemy_X_Position,x - lda #1 ; default: face right + lda #1 ; default: face right bcs @store - lda #2 ; player is left of boss: face left + lda #2 ; player is left of boss: face left @store: sta Enemy_MovingDir,x sta EnemyFacingDir,x @@ -524,17 +673,10 @@ BossJump: .segment "PRG7" .reloc ; Draw the shadow-mario boss (and, eventually, its projectiles) into OAM. -; Hooked at $9a7f, which is part of the bank5 dark-link state machine and -; runs *independently* of PatchLinkDrawRoutine, so the boss no longer -; flickers in lockstep with Mario's hurt/invincibility flicker. +; Hooked at $9a7f, which is part of the bank5 dark-link state machine ; ; DrawMetasprite lives in PRG0, so we save the current bank, swap in PRG0, -; do the draw, then restore the bank that was loaded at $8000/$A000 on -; entry (typically bank5, since we were called from bank5 code). -; -; Boss sprites are written starting at OAM byte 48 - past the slots that -; PatchLinkDrawRoutine uses for the player + shield + Mario projectiles - -; so neither call clobbers the other regardless of which runs first. +; do the draw, then restore the bank to bank5 after BOSS_OAM_OFFSET = 48 BossGraphicsHandler: @@ -564,7 +706,14 @@ BossGraphicsHandler: lda #2 ; palette 3 attribute (shadow mario palette) sta SprObject_SprAttrib,y ldx boss_animation - jsr DrawMetasprite + + cpx #SKIDDING + bne @nm_no_skid_flip + jsr DrawMetasprite + jmp @nm_drew +@nm_no_skid_flip: + jsr DrawMetasprite +@nm_drew: jsr BossDrawProjectiles ; shadow fireballs + hammers (palette already set per-slot) @restore: @@ -578,27 +727,11 @@ BossGraphicsHandler: .segment "PRG5", "PRG7" -; --- Boss projectile system --------------------------------------------------- -; Boss projectiles live in SprObject indices 7 and 8 - the slots between the 6 -; vanilla enemy slots and Mario's fireball/hammer pair. DrawMetasprite reads -; SprObject_*,y directly so once a slot is populated we can render it with the -; existing METASPRITE_FIREBALL_FRAME_x / METASPRITE_HAMMER_FRAME_x metasprites. -; -; A slot is active when EnemyProjectileType,n != 0 (zero = inactive). We never -; leave a metasprite ID stored at $87..$88 outside of our own update/draw paths -; so vanilla projectile dispatch can't mistake it for a Z2 projectile type. -; -; We co-opt one bit of SprObject_SprAttrib,n to remember the type: -; bit 6 = 1 -> hammer (lobbed, gravity) -; bit 6 = 0 -> fireball (straight horizontal) -; Bits 0-1 stay set to %10 = palette 3 (shadow palette) for both types. - -PROJ_ATTR_FIREBALL = %00000010 ; palette 3 -PROJ_ATTR_HAMMER = %01000010 ; palette 3 + "is hammer" flag (uses OAM bit 6, harmless on render) +PROJ_TYPE_HAMMER = %00010000 +PROJ_ATTR_FIREBALL = %00000001 +PROJ_ATTR_HAMMER = PROJ_ATTR_FIREBALL | PROJ_TYPE_HAMMER .reloc -; FindFreeBossProjSlot: returns Y = 7..(7+ENEMY_PROJECTILE_COUNT-1) for an empty slot, -; or Y < 7 with N flag set if none available. Active = EnemyProjectileType,Y != 0. FindFreeBossProjSlot: ldy #EnemyProjectileOffset @loop: @@ -607,8 +740,7 @@ FindFreeBossProjSlot: iny cpy #EnemyProjectileOffset + ENEMY_PROJECTILE_COUNT bcc @loop - ldy #0 - dey ; Y = $FF, N set, BCC won't take + ldy #$ff @found: rts @@ -645,8 +777,10 @@ BossSpawnFireball: @right: lda #3 ; right sta SprObject_X_Speed,y - lda #0 - sta SprObject_Y_Speed,y ; fireballs travel level + ; Small initial downward Y speed; gravity in BossUpdateProjectiles adds + ; from there and BG bounce reverses Y_Speed when the floor is reached. + lda #1 + sta SprObject_Y_Speed,y lda #PROJ_ATTR_FIREBALL sta SprObject_SprAttrib,y @@ -715,13 +849,24 @@ BossSpawnHammer: @done: rts +; Y position at which fireballs bounce. This is the visual floor of the +; dark-mario arena (one metatile below the boss's typical foot Y). +FIREBALL_GROUND_Y = $B8 +FIREBALL_BOUNCE_Y_SPEED = <(-4) +PROJ_MAX_FALL_SPEED = 4 + .reloc -; BossUpdateProjectiles: per-frame movement + offscreen kill + collision check -; for each active boss projectile. Hammers gain +1 to Y velocity each frame -; (gravity); fireballs ignore Y velocity. +; BossUpdateProjectiles: per-frame motion + offscreen kill + collision check +; for each active boss projectile. Both fireballs and hammers integrate Y +; velocity and accumulate gravity every other frame (capped at +; PROJ_MAX_FALL_SPEED). Fireballs additionally bounce off +; FIREBALL_GROUND_Y by reversing Y_Speed - matching the SMB1 fireball that +; skips along the floor. Hammers fall through that line and die when they +; drop off the bottom of the screen. BossUpdateProjectiles: ldy #EnemyProjectileOffset @loop: + sty $11 lda EnemyProjectileType - EnemyProjectileOffset,y bne @active jmp @next @@ -732,22 +877,50 @@ BossUpdateProjectiles: adc SprObject_X_Speed,y sta SprObject_X_Position,y - ; Hammer arc: integrate Y speed and apply gravity - lda SprObject_SprAttrib,y - and #%01000000 ; PROJ_ATTR_HAMMER bit - beq @move_done + ; Y position += Y speed (both projectile types) + clc + lda SprObject_Y_Position,y + adc SprObject_Y_Speed,y + sta SprObject_Y_Position,y + + ; Gravity: every other frame, add 1 to Y_Speed. + ; - Hammers (bit 6 of SprAttrib set): uncapped - they keep accelerating + ; downward until they fall off the bottom of the screen and die. + ; - Fireballs: cap on the *positive* (downward) side at + ; PROJ_MAX_FALL_SPEED so bounces don't accelerate without bound. + ; Negative Y_Speed (rising fireball post-bounce) always pulls toward 0; + ; the unsigned cap check would otherwise treat $FC as ">= 4" and skip + ; gravity entirely, leaving the fireball travelling level forever. + lda FrameCounter + lsr + bcs @after_gravity + lda SprObject_SprAttrib,y + and #PROJ_TYPE_HAMMER + bne @apply_gravity ; hammer: uncapped + lda SprObject_Y_Speed,y + bmi @apply_gravity ; rising fireball: always pull toward 0 + cmp #PROJ_MAX_FALL_SPEED + bcs @after_gravity ; fireball at terminal: skip + @apply_gravity: clc + lda SprObject_Y_Speed,y + adc #1 + sta SprObject_Y_Speed,y + @after_gravity: + + ; Fireball-only: bounce when reaching the arena floor. Hammers (bit 4 + ; PROJ_TYPE_HAMMER set) skip this and keep falling until offscreen. + lda SprObject_SprAttrib,y + and #PROJ_TYPE_HAMMER + bne @no_bounce lda SprObject_Y_Position,y - adc SprObject_Y_Speed,y + cmp #FIREBALL_GROUND_Y + bcc @no_bounce + lda #FIREBALL_GROUND_Y sta SprObject_Y_Position,y - ; Apply gravity once every 2 frames so hammers don't sink too fast - lda FrameCounter - lsr - bcs @move_done - lda SprObject_Y_Speed,y - adc #1 - sta SprObject_Y_Speed,y - @move_done: + lda #FIREBALL_BOUNCE_Y_SPEED + sta SprObject_Y_Speed,y + @no_bounce: ; Offscreen kill: remove if X out of [0, 248] or Y >= $D8 (below floor). lda SprObject_X_Position,y @@ -767,8 +940,7 @@ BossUpdateProjectiles: sta $05 lda #8 sta $07 - sty $11 ; preserve loop slot - jsr bank7_LoadLinkHitbox ; Mario body in $00-$03 + jsr bank7_LoadLinkHitbox jsr bank7_CollisionTest ldy $11 bcc @next @@ -783,9 +955,12 @@ BossUpdateProjectiles: lda #0 sta EnemyProjectileType - EnemyProjectileOffset,y @next: + ldy $11 iny cpy #EnemyProjectileOffset + ENEMY_PROJECTILE_COUNT - bcc @loop + bcs @skip + jmp @loop + @skip: ldx #0 rts @@ -800,29 +975,52 @@ BossDrawProjectiles: @loop: lda EnemyProjectileType - EnemyProjectileOffset,y beq @next - ; Reload animation frame (frame_1 / frame_2) based on FrameCounter. - lda SprObject_SprAttrib,y - and #%01000000 - bne @hammer_anim - lda FrameCounter - and #%00000100 + ; SMB1-style 16-frame rotation cycle, mirroring how vanilla Mario's + ; fireballs / hammers spin: + ; FC bit 2: pick FRAME_1 vs FRAME_2 (toggles every 4 frames) + ; FC bit 3: pick "no flip" vs "H+V flip" (toggles every 8 frames) + ; The four resulting (frame, flip) combinations cycle the sprite + ; through up-right / down-right / down-left / up-left orientations + ; so the projectile visibly tumbles in flight. + + ; --- Frame selection --- + lda FrameCounter + and #%00000100 + bne @use_frame_2 + lda SprObject_SprAttrib,y + and #PROJ_TYPE_HAMMER beq @set_fb1 - lda #METASPRITE_FIREBALL_FRAME_2 - .byte $2c - @set_fb1: - lda #METASPRITE_FIREBALL_FRAME_1 - jmp @do_draw - @hammer_anim: - lda FrameCounter - and #%00001000 - beq @set_h1 - lda #METASPRITE_HAMMER_FRAME_2 - .byte $2c - @set_h1: - lda #METASPRITE_HAMMER_FRAME_1 - @do_draw: + lda #METASPRITE_HAMMER_FRAME_1 + .byte $2c ; BIT abs - skip the next two bytes + @set_fb1: + lda #METASPRITE_FIREBALL_FRAME_1 + jmp @set_frame + @use_frame_2: + lda SprObject_SprAttrib,y + and #PROJ_TYPE_HAMMER + beq @set_fb2 + lda #METASPRITE_HAMMER_FRAME_2 + .byte $2c + @set_fb2: + lda #METASPRITE_FIREBALL_FRAME_2 + @set_frame: sta EnemyProjectileType - EnemyProjectileOffset,y - tax + + ; --- Flip selection --- + ; Clear OAM flip bits while preserving palette + PROJ_TYPE_HAMMER. + lda SprObject_SprAttrib,y + and #%00111111 + sta SprObject_SprAttrib,y + lda FrameCounter + and #%00001000 + beq @no_flip + lda SprObject_SprAttrib,y + ora #%11000000 ; H + V flip + sta SprObject_SprAttrib,y + @no_flip: + + ; --- Draw --- + ldx EnemyProjectileType - EnemyProjectileOffset,y sty $11 jsr DrawMetasprite ldy $11 @@ -960,13 +1158,17 @@ InjuredBossJump: lda #$10 ; Sword stab SFX sta Z2NoiseSoundQueue ; stab SFX + - lda #30 ; 0.5 seconds at 60 fps + lda #60 ; 1 second of invincibility at 60 fps sta boss_invincibility_timer + ; A successful stomp should also short-circuit any standing pause the boss + ; was in - if the player can land a hit, the boss should react / counter + ; immediately rather than continuing to wind down from the previous state. + lda #0 + sta boss_transition_delay ; Reset state timer if currently idle (mirrors InjuredBoss behavior) lda boss_state bne @done - lda #0 - sta boss_state_timer + sta boss_state_timer ; A is already 0 here @done: rts @@ -974,28 +1176,26 @@ InjuredBossJump: .reloc .import ImposeGravitySprObj MoveD_EnemyVertically: - lda NmiBankShadow8 - pha - lda NmiBankShadowA - pha - - lda #0 - jsr SwapPRG - - ldy #$3d ;set quick movement amount downwards - lda #$03 ;set maximum speed in A - sty R0 ;set movement amount here - inx ;increment X for enemy offset - - jsr ImposeGravitySprObj ;do a sub to move enemy object downwards - pla - sta NmiBankShadowA - sta PrgBankAReg - pla - sta NmiBankShadow8 - sta PrgBank8Reg - ldx #0 ;boss is always at enemy slot 0 - rts + lda NmiBankShadow8 + pha + lda NmiBankShadowA + pha + lda #0 + jsr SwapPRG + ldy #$3d ;set quick movement amount downwards + lda #$03 ;set maximum speed in A + sty R0 ;set movement amount here + inx ;increment X for enemy offset + + jsr ImposeGravitySprObj ;do a sub to move enemy object downwards + pla + sta NmiBankShadowA + sta PrgBankAReg + pla + sta NmiBankShadow8 + sta PrgBank8Reg + ldx #0 ;boss is always at enemy slot 0 + rts ;.reloc ;MoveEnemyHorizontally: diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index 50a33326f..75a78e16e 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -574,6 +574,7 @@ RenderLoop: ora (Ptr),y bne WritePalette ; unconditional AllowPaletteChange: + and #%11111100 ora Atr WritePalette: sta Sprite_Attributes,x diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index f4efaba24..7e6e02458 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -304,6 +304,7 @@ RESV boss_substate RESV boss_counter RESV boss_freeze RESV boss_ChrBank +RESV boss_transition_delay ; nonzero = boss is paused mid-state-switch (frames remaining) BOSS_RAM_SIZE = RES_OFFSET - CUSTOM_BOSS_START - 1 EnemyProjectileOffset = 7 From ca40299f15a94a066cab8886146e36fe7d83323f Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 11 May 2026 18:52:14 -0400 Subject: [PATCH 025/128] Finish boss design --- RandomizerCore/Asm/z2mario/boss.s | 289 +++++++++++++++--------------- 1 file changed, 142 insertions(+), 147 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index ebdcfe25a..f27c039e3 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -25,10 +25,6 @@ mus_bossdie = 8 ; Frames the boss spends standing still after each top-level state change. STATE_SWITCH_DELAY = 50 -; Initial speed kicked in when MovBossRunning starts from a dead stop (e.g. -; after a transition delay zeroed X_Speed, or after dropping off the platform -; into the middle of the arena). -RUNNING_KICK_SPEED = $20 ; Right-side platform target: the great-palace dark-link arena has a 3-metatile-wide ; platform on the right, 7 metatiles above the ground. Boss will jump up here for a hammer attack @@ -42,8 +38,15 @@ JUMP_Y_SPEED = <(-8) ; initial upward velocity (matches old straight- ; floor of the arena sits at $A0+, the platform sits 7 metatiles higher). PLATFORM_TOP_Y_THRESHOLD = $80 -LEFT_EDGE_TARGET = 6*16 -RIGHT_EDGE_TARGET = 9*16 +LEFT_EDGE_TARGET = 3*16 +RIGHT_EDGE_TARGET = 10*16 + +; Boss running model: accelerate to BOSS_MAX_RUN_SPEED while far from the +; target wall; when within BOSS_DECEL_DISTANCE pixels of the target, +; decelerate to zero and flip via MovChangeState. +BOSS_MAX_RUN_SPEED = $50 +BOSS_DECEL_DISTANCE = 3 * 16 ; in metatiles + .segment "PRG7" ; patch the vanilla enemy projectile check to prevent it from trying to run our hammer/fireball @@ -181,20 +184,25 @@ EnemyLanding: .segment "PRG5", "PRG7" .reloc RunBoss: - lda #0 - sta $de ; unfreeze link - lda boss_freeze - beq @notfreezed - dec boss_freeze - lda #STANDING - sta boss_animation - ; Draw every other frame (flicker) - lda FrameCounter - lsr - bcs @nodraw2 -; jsr RelativeEnemyPosition - ; jsr BossGraphicsHandler - @nodraw2: + lda #0 + sta $de ; unfreeze link + + ; Display boss HP bar with a divisor of 7 (close enough to 60 HP) + lda #7 + jsr $A4E9 + + lda boss_freeze + beq @notfreezed + dec boss_freeze + lda #STANDING + sta boss_animation + ; Draw every other frame (flicker) +; lda FrameCounter +; lsr +; bcs @nodraw2 +; ; jsr RelativeEnemyPosition +; ; jsr BossGraphicsHandler +; @nodraw2: ldx #0 rts @@ -262,23 +270,13 @@ AnimateKilledBoss: .reloc HandleBossMovement: ; If the boss is invincible (just got stomped), force-clear any pending - ; transition delay. When a stomp sets boss_state_timer = 0, the next - ; frame's MovBossSide transition re-arms STATE_SWITCH_DELAY - clearing - ; it here every invincibility frame guarantees the boss skips the - ; standing pause when an attack is cancelled by a hit. + ; transition delay. lda boss_invincibility_timer beq + lda #0 sta boss_transition_delay + - ; If a transition delay is in flight, the boss just left an attack and - ; is winding down before the next state runs. Two cases: - ; - Airborne (e.g. mid-leap off the platform): freeze the timer and - ; keep applying X velocity so the jump arc completes. The timer - ; only ticks once we've landed; otherwise zeroing X_Speed strands - ; him over the same column and he never clears the platform edge. - ; - Grounded: stand still, zero X speed, tick the timer down. lda boss_transition_delay beq @run_state lda Enemy_State,x @@ -292,9 +290,6 @@ HandleBossMovement: sta Enemy_X_Speed,x rts @airborne_delay: - ; In flight: hold the timer, finish the arc. X velocity was set by - ; the post-attack transition; gravity is applied in RunBoss after - ; HandleBossMovement returns. lda #JUMPING sta boss_animation jmp MoveEnemyHorizontally @@ -367,75 +362,108 @@ MovBossSide: MovBossRunning: jsr BossSetWalkingAnim - lda Enemy_X_Speed,x - bne @speed_ok - lda Enemy_X_Position,x - cmp #LEFT_EDGE_TARGET - bcc @speed_ok ; left wall: leave to @Deaccelerating - cmp #RIGHT_EDGE_TARGET - bcs @speed_ok ; right wall: leave to @Deaccelerating - lda Enemy_MovingDir,x - cmp #1 - beq @kick_right - lda #<(-RUNNING_KICK_SPEED) - .byte $2c - @kick_right: - lda #RUNNING_KICK_SPEED - sta Enemy_X_Speed,x -@speed_ok: + ; Airborne: skip the accel/decel state machine. Just integrate the + ; jump-off velocity so the parabola off the platform stays clean. + lda Enemy_State,x + and #%01000000 + beq @grounded + jmp MoveEnemyHorizontally +@grounded: + ; One-shot bridge jump at the start of a run (set by MovBossSide via + ; inc boss_bridge when transitioning from idle). Fires once on the + ; first grounded frame, sets the airborne flag, then defers further + ; AI work until next frame. + lda boss_bridge + beq @no_bridge + dec boss_bridge + lda #<(-4) + sta Enemy_Y_Speed,x + jsr BossJump + jmp @move ; airborne now; let the arc start +@no_bridge: + + ; Compute unsigned distance from the wall the boss is heading toward. + ; Facing right -> target = RIGHT_EDGE_TARGET, distance = target - X + ; Facing left -> target = LEFT_EDGE_TARGET, distance = X - target + ; If the position has already crossed the target (subtract underflows), + ; clamp distance to 0 so the decel branch runs and we flip immediately. lda Enemy_MovingDir,x - cmp #$01 - beq MovBossRight + cmp #1 + beq @facing_right - ; Moving left lda Enemy_X_Position,x - cmp #RIGHT_EDGE_TARGET - bcs @StillAccer - cmp #LEFT_EDGE_TARGET - bcs MovStillMoving - @Deaccelerating: + sec + sbc #LEFT_EDGE_TARGET + bcs + + lda #0 + + + cmp #BOSS_DECEL_DISTANCE + bcs @accel_left + jmp @decel_left + +@facing_right: + lda #RIGHT_EDGE_TARGET + sec + sbc Enemy_X_Position,x + bcs + + lda #0 + + + cmp #BOSS_DECEL_DISTANCE + bcs @accel_right + jmp @decel_right + +@accel_left: + ; Push X_Speed toward -BOSS_MAX_RUN_SPEED at 2/frame. + lda Enemy_X_Speed,x + bpl @do_dec_l ; positive or zero: dec unconditionally + cmp #<(-BOSS_MAX_RUN_SPEED) + 1 + bcc @move ; already at/past target (more negative) +@do_dec_l: + dec Enemy_X_Speed,x + dec Enemy_X_Speed,x + jmp @move + +@accel_right: + ; Push X_Speed toward +BOSS_MAX_RUN_SPEED at 2/frame. + lda Enemy_X_Speed,x + bmi @do_inc_r ; negative: inc unconditionally + cmp #BOSS_MAX_RUN_SPEED + bcs @move ; already at/past target +@do_inc_r: + inc Enemy_X_Speed,x + inc Enemy_X_Speed,x + jmp @move + +@decel_left: lda #SKIDDING sta boss_animation + lda Enemy_X_Speed,x + beq @flip ; already stopped + bpl @flip ; wrong-sign (positive while facing left) inc Enemy_X_Speed,x inc Enemy_X_Speed,x lda Enemy_X_Speed,x - bmi MovStillMoving - jmp MovChangeState - @StillAccer: - dec Enemy_X_Speed,x - dec Enemy_X_Speed,x - jmp MovStillMoving + bmi @move ; still moving left: continue + jmp @flip ; crossed zero / reached zero: flip -MovBossRight: - lda Enemy_X_Position,x - cmp #LEFT_EDGE_TARGET - bcc @StillAccer - cmp #RIGHT_EDGE_TARGET - bcc MovStillMoving - @Deaccelerating: +@decel_right: lda #SKIDDING sta boss_animation + lda Enemy_X_Speed,x + beq @flip + bmi @flip dec Enemy_X_Speed,x dec Enemy_X_Speed,x lda Enemy_X_Speed,x - bpl MovStillMoving + beq @flip + bmi @flip + jmp @move ; still moving right: continue + +@flip: jmp MovChangeState - @StillAccer: - inc Enemy_X_Speed,x - inc Enemy_X_Speed,x -MovStillMoving: - lda boss_bridge - beq @no_jump - dec boss_bridge - lda Enemy_State,x - and #%01000000 - bne @no_jump - lda #<(-4) - sta Enemy_Y_Speed,x - jsr BossJump - @no_jump: +@move: jmp MoveEnemyHorizontally MovChangeState: @@ -471,24 +499,18 @@ MovBossMiddle: bcc @loop @no_need_for_p: - ; Face Mario before launching off the platform. - jsr BossFacePlayer + ; Face boss left before jumping off platform lda #0 sta boss_bridge - ; Set running speed in the (now player-facing) direction. - lda Enemy_MovingDir,x - lsr - bcs @right - lda #<(-$20) - .byte $2c - @right: - lda #$20 + lda #2 + sta Enemy_MovingDir,x + sta EnemyFacingDir,x + + ; Small hop off the platform + lda #<(-(20)) sta Enemy_X_Speed,x - ; Step off the platform: nudge upward and mark airborne so RunBoss's - ; gravity path engages. Without this, the boss might not make it off the platform - ; and then start the run phase while at the top of the screen lda #<(-3) sta Enemy_Y_Speed,x jsr BossJump @@ -508,12 +530,11 @@ MovBossMiddle: ldx #0 @on_platform: - ; Every 24 frames, lob a shadow hammer from the boss with a random - ; X-speed; BossSpawnHammer biases the throw toward Mario. + ; Every 24 frames, lob a hammer from the boss with a random speed lda FrameCounter and #%00010111 bne @done - jsr BossSpawnHammer + jmp BossSpawnHammer @done: rts @@ -628,9 +649,9 @@ BossFacePlayer: ldx #0 lda Player_X_Position cmp Enemy_X_Position,x - lda #1 ; default: face right + lda #1 ; face right bcs @store - lda #2 ; player is left of boss: face left + lda #2 ; player is left of boss @store: sta Enemy_MovingDir,x sta EnemyFacingDir,x @@ -814,11 +835,13 @@ BossSpawnHammer: lda #1 sta SprObject_Y_HighPos,y - ; Random X-speed magnitude in 1..4, sign points toward Mario. + ; Random X-speed magnitude in 0..5, sign points toward Mario. lda RNG - and #$03 - clc - adc #1 + and #7 + cmp #6 + bcc + + and #3 ; if its 6 or 7 then just make it 2 or 3 instead + + pha lda Player_X_Position cmp Enemy_X_Position,x @@ -856,13 +879,6 @@ FIREBALL_BOUNCE_Y_SPEED = <(-4) PROJ_MAX_FALL_SPEED = 4 .reloc -; BossUpdateProjectiles: per-frame motion + offscreen kill + collision check -; for each active boss projectile. Both fireballs and hammers integrate Y -; velocity and accumulate gravity every other frame (capped at -; PROJ_MAX_FALL_SPEED). Fireballs additionally bounce off -; FIREBALL_GROUND_Y by reversing Y_Speed - matching the SMB1 fireball that -; skips along the floor. Hammers fall through that line and die when they -; drop off the bottom of the screen. BossUpdateProjectiles: ldy #EnemyProjectileOffset @loop: @@ -884,23 +900,16 @@ BossUpdateProjectiles: sta SprObject_Y_Position,y ; Gravity: every other frame, add 1 to Y_Speed. - ; - Hammers (bit 6 of SprAttrib set): uncapped - they keep accelerating - ; downward until they fall off the bottom of the screen and die. - ; - Fireballs: cap on the *positive* (downward) side at - ; PROJ_MAX_FALL_SPEED so bounces don't accelerate without bound. - ; Negative Y_Speed (rising fireball post-bounce) always pulls toward 0; - ; the unsigned cap check would otherwise treat $FC as ">= 4" and skip - ; gravity entirely, leaving the fireball travelling level forever. lda FrameCounter lsr bcs @after_gravity lda SprObject_SprAttrib,y and #PROJ_TYPE_HAMMER - bne @apply_gravity ; hammer: uncapped + bne @apply_gravity ; hammer: uncapped lda SprObject_Y_Speed,y - bmi @apply_gravity ; rising fireball: always pull toward 0 + bmi @apply_gravity ; rising fireball: always pull toward 0 cmp #PROJ_MAX_FALL_SPEED - bcs @after_gravity ; fireball at terminal: skip + bcs @after_gravity ; fireball at terminal: skip @apply_gravity: clc lda SprObject_Y_Speed,y @@ -930,21 +939,22 @@ BossUpdateProjectiles: cmp #$D8 bcs @kill + ; Skip collision detection if mario can't be hurt right now + lda InjuryTimer + bne @next ; Collision with player. Boss projectile box: 8x8 at (X,Y). Reuse the ; vanilla player hitbox loader for $00-$03, and hand-build $04-$07. lda SprObject_X_Position,y sta $04 - lda #8 - sta $06 lda SprObject_Y_Position,y sta $05 lda #8 + sta $06 sta $07 jsr bank7_LoadLinkHitbox jsr bank7_CollisionTest ldy $11 bcc @next - ; Hit Mario - flag him for damage and drop projectile ldx #0 lda $a8,x ora #$10 @@ -966,24 +976,11 @@ BossUpdateProjectiles: .segment "PRG7" .reloc -; BossDrawProjectiles: invoked from BossGraphicsHandler with PRG0 already paged -; in, so DrawMetasprite is reachable. This routine MUST live in PRG7 (fixed -; bank) so it stays callable after BossGraphicsHandler's SwapPRG(0) replaces -; PRG5 at $8000. Animation frame toggles every 4-8 frames. BossDrawProjectiles: ldy #EnemyProjectileOffset @loop: lda EnemyProjectileType - EnemyProjectileOffset,y beq @next - ; SMB1-style 16-frame rotation cycle, mirroring how vanilla Mario's - ; fireballs / hammers spin: - ; FC bit 2: pick FRAME_1 vs FRAME_2 (toggles every 4 frames) - ; FC bit 3: pick "no flip" vs "H+V flip" (toggles every 8 frames) - ; The four resulting (frame, flip) combinations cycle the sprite - ; through up-right / down-right / down-left / up-left orientations - ; so the projectile visibly tumbles in flight. - - ; --- Frame selection --- lda FrameCounter and #%00000100 bne @use_frame_2 @@ -991,7 +988,7 @@ BossDrawProjectiles: and #PROJ_TYPE_HAMMER beq @set_fb1 lda #METASPRITE_HAMMER_FRAME_1 - .byte $2c ; BIT abs - skip the next two bytes + .byte $2c @set_fb1: lda #METASPRITE_FIREBALL_FRAME_1 jmp @set_frame @@ -1015,11 +1012,11 @@ BossDrawProjectiles: and #%00001000 beq @no_flip lda SprObject_SprAttrib,y - ora #%11000000 ; H + V flip + ora #%11000000 ; H + V flip sta SprObject_SprAttrib,y @no_flip: - ; --- Draw --- + ; Keep the current object we are processing around in $11 for the next loop ldx EnemyProjectileType - EnemyProjectileOffset,y sty $11 jsr DrawMetasprite @@ -1072,7 +1069,6 @@ StompedBoss: adc #10 cmp Enemy_Y_Position,x bcc @valid_stomp -; jmp StompInjury ; player below boss -> normal stomp injury to PLAYER jmp DamagePlayer @valid_stomp: lda #$fd @@ -1138,7 +1134,6 @@ BossPlayerCollision: sta $a8,x jmp bank7_LinkCollision ; processes $A8 flag to damage Mario; returns to RunBoss caller - .reloc ; Variant of InjuredBoss that subtracts 5 HP at once (jump attack damage). ; HP is floored at 0 so the KillBoss death-countdown path runs normally. @@ -1154,7 +1149,7 @@ InjuredBossJump: + sta Enemy_HP,x beq + - ; don't play sfx for last hit + ; don't play sfx for last hit. works around a weird bug i didn't bother to fix lda #$10 ; Sword stab SFX sta Z2NoiseSoundQueue ; stab SFX + From 134d4a2fb50e5b7c41674423ca4c875c4e380795 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 11 May 2026 18:53:05 -0400 Subject: [PATCH 026/128] Disable demo code and remove shadow fireball hack --- RandomizerCore/Asm/z2mario/mario.s | 34 ++++++++++++------------- RandomizerCore/Asm/z2mario/metasprite.s | 1 - 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 38c3ab258..f3023a588 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -21,23 +21,23 @@ .proc GameRoutines ;.ifdef DEMO_CODE - lda #1 - sta $0785 ;candle - sta $078c ;key - sta $078b ;hammer - sta $0788 ;boots - sta $0787 ;raft - sta $077b ; shield spell - sta $077c ; jump spell - sta $077d ; life spell - sta $077e ; fairy spell - sta $077f ; fire spell - sta $0780 ; reflect spell - sta $0781 ; spell spell - sta $0782 ; thunder spell - lda #8 - sta $0783 ; magic jar count - sta $0778 ; magic level + ; lda #1 + ; sta $0785 ;candle + ; sta $078c ;key + ; sta $078b ;hammer + ; sta $0788 ;boots + ; sta $0787 ;raft + ; sta $077b ; shield spell + ; sta $077c ; jump spell + ; sta $077d ; life spell + ; sta $077e ; fairy spell + ; sta $077f ; fire spell + ; sta $0780 ; reflect spell + ; sta $0781 ; spell spell + ; sta $0782 ; thunder spell + ; lda #8 + ; sta $0783 ; magic jar count + ; sta $0778 ; magic level ; lda #$ff ; sta $0773 ;infinite magic diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index 75a78e16e..50a33326f 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -574,7 +574,6 @@ RenderLoop: ora (Ptr),y bne WritePalette ; unconditional AllowPaletteChange: - and #%11111100 ora Atr WritePalette: sta Sprite_Attributes,x From 2b5f4b403634725fe5bab9ef7eab7a4ddb0e7e47 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 11 May 2026 18:53:26 -0400 Subject: [PATCH 027/128] Reset the custom music tracks on death --- RandomizerCore/Asm/StatTracking.s | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RandomizerCore/Asm/StatTracking.s b/RandomizerCore/Asm/StatTracking.s index d174bc009..db648a433 100644 --- a/RandomizerCore/Asm/StatTracking.s +++ b/RandomizerCore/Asm/StatTracking.s @@ -19,6 +19,11 @@ CONVERT = $80 LinkJustDied: inc StatDeaths inc $0494 + ; Patch the death routine code to clear the previous/current FT tracks so they restart after death + ; I just think it sounds better like that + lda #$ff + sta $69e5 ; PrevFtTrack + sta $69f4 ; SavedFtTrack rts ; Update all locations that sets $0494 - link just died From 8833197bf11dc4d158d55f841106e9ed1462a1c0 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 11 May 2026 18:57:07 -0400 Subject: [PATCH 028/128] Remove unused boss fireball check --- RandomizerCore/Asm/z2mario/boss.s | 6 ------ RandomizerCore/Asm/z2mario/integration.s | 22 ++-------------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index f27c039e3..978e9ab11 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -99,12 +99,6 @@ BossLandedInCutscene: jmp $9a77 FREE_UNTIL $9a0f - -; Patch bank5_Enemy_Vulnerability_Damage_Codes entry $23 (Dark Link): -; Clear bit 5 "Immune to Flying Blade and Fire" so fireballs can hit the boss. -.org $951C - .byte $14 - ; Make dark mario use a different hitbox (vanilla has a jank $0e hitbox, switch ; to a simple tall enemy hitbox with $00) .org $9540 diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 69f49c8cb..e996d6b6b 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -5,7 +5,6 @@ .import ProcHammerTime .import Square1SfxHandler, Square2SfxHandler, NoiseSfxHandler .import SwapToSavedPRG, SwapToPRG0 -.import InjuredBoss .export SlightlyModifiedCollisionRoutine ; Remove unused code after we gutted link's movement @@ -626,28 +625,11 @@ PatchFireballHitcheck: bne @IsHammer lda #1 sta $0b ; 1 = fireball - bne @CheckBoss ; always taken + bne @CollisionCheck ; always taken @IsHammer: lda #0 sta $0b ; 0 = hammer (pass-through) -@CheckBoss: - ; Boss entity (type $23): bypass $E694 to avoid vanilla bank7_monster_death on HP=0 - lda $a1,x - cmp #$23 - bne @NotBoss - jsr bank7_LoadObjectHitbox ; load boss hitbox ZP $04-$07 - jsr bank7_CollisionTest ; test projectile ($00-$03) vs boss ($04-$07) - bcc @exit ; miss - jsr InjuredBoss ; decrement HP via controlled path, start invincibility - ldy $11 - lda Fireball_State,y - and #%01000000 - bne @exit ; hammer passes through; don't despawn it - lda #%10000000 ; fireball: trigger explosion state - sta Fireball_State,y -@exit: - rts -@NotBoss: +@CollisionCheck: jsr $E694 bcc @NotBossExit ldy $11 From 4c2d6d7ce2d552b8c0ded76ae261c2929ce6f8ac Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 11 May 2026 18:57:26 -0400 Subject: [PATCH 029/128] Fixup the title screen to properly support removing the RANDOMIZER line --- RandomizerCore/Hyrule.cs | 14 +++++- RandomizerCore/ROM.cs | 104 ++++++++++++++++++++++++++------------- 2 files changed, 82 insertions(+), 36 deletions(-) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 76078f8cf..4e548d4da 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2419,7 +2419,15 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra } rom.UpdateSprite(props.CharSprite, true, props.ChangeItemSprites); rom.UpdateSpritePalette(props.TunicColor, props.SkinTone, props.OutlineColor, props.ShieldColor, props.BeamSprite); - rom.Put(ROM.ChrRomOffset + 0x01000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.randomizer_text.chr")); + const bool isRandomized = false; + if (isRandomized) + rom.Put(ROM.ChrRomOffset + 0x01000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.randomizer_text.chr")); + else + { + // We need to move the "OF" graphics over the unused zelda graphics + // to fit the new MARIO text (whether its used or not) + rom.Put(ROM.ChrRomOffset + 0x1000, rom.GetBytes(ROM.ChrRomOffset + 0x1380, 0x13e0 - 0x1380)); + } if (props.MarioMode) { // Overwrite link's downstab animation with fireball explosion sprites @@ -3823,7 +3831,9 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando bool randomizeMusic = !props.DisableMusic && props.RandomizeMusic; ChangeMapperToMMC5(engine, props.DisableHUDLag, randomizeMusic, props.MarioMode); // will make output vary with customize tab options - rom.AddRandomizerToTitle(engine, props.MarioMode); + // the second flag determines if we should add the "randomizer" text, but we don't have a "vanilla" option + // right now, so this is just always off, except when i manually make an update + rom.AddRandomizerToTitle(engine, props.MarioMode, false); AddCropGuideBoxesToFileSelect(engine); FixHelmetheadBossRoom(engine); FullItemShuffle(engine, GetNonSideviewItemLocations()); diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 3a5407ede..0a93ebe99 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -434,12 +434,78 @@ public void WritePalacePalettes(List bricks, List curtains, List Date: Tue, 12 May 2026 15:41:53 -0400 Subject: [PATCH 030/128] Skip bounce recoil when processing projectiles --- CommandLine/Properties/launchSettings.json | 2 +- RandomizerCore/Asm/z2mario/integration.s | 60 ++++------------------ RandomizerCore/Asm/z2r.inc | 1 + 3 files changed, 13 insertions(+), 50 deletions(-) diff --git a/CommandLine/Properties/launchSettings.json b/CommandLine/Properties/launchSettings.json index 34681a8d9..2593b1149 100644 --- a/CommandLine/Properties/launchSettings.json +++ b/CommandLine/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "CommandLine": { "commandName": "Project", - "commandLineArgs": "-f AAABvvyhAmAAAAAAAAAATy9Q#AAAAAAAAAAAAACRA -r ../Zelda2.nes -s 32165874 -o ..", + "commandLineArgs": "-f AAABvvyhAmLAAAAAAAAAEuPk+hAAAAAAAAAAAAAmA -r ../Zelda2.nes -s 32165874 -o ..", "workingDirectory": "../build/" } } diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index e996d6b6b..0392dd94a 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -630,15 +630,17 @@ PatchFireballHitcheck: lda #0 sta $0b ; 0 = hammer (pass-through) @CollisionCheck: + inc ProjectileProcessing jsr $E694 - bcc @NotBossExit + dec ProjectileProcessing + bcc @NoCollision ldy $11 lda Fireball_State,y and #%01000000 - bne @NotBossExit ; hammer: pass through + bne @NoCollision ; hammer: pass through lda #%10000000 sta Fireball_State,y -@NotBossExit: +@NoCollision: rts ; Skip over stopping the projectile @@ -804,15 +806,10 @@ PatchFinishPlayerPaletteCycle: ; Update palette locations -;.ifdef DEMO_CODE -; UPDATE_BYTE $37 @ $285a $2a0a $40af $40bf $40cf $40df $80af $80bf $80cf $80df $c0af $c0bf $c0cf $c0df $c0ef $100af $100bf $100cf $100df $140af $140bf $140cf $140df $17c19 $1c464 $1c47c -; UPDATE_BYTE $27 @ $285b $2a0b $2a10 $40b0 $40c0 $40d0 $40e0 $80b0 $80c0 $80d0 $80e0 $c0b0 $c0c0 $c0d0 $c0e0 $c0f0 $100b0 $100c0 $100d0 $100e0 $140b0 $140c0 $140d0 $140e0 $17c1a $1c465 $1c47d -; UPDATE_BYTE $16 @ $285C $40b1 $40c1 $40d1 $80e1 $80b1 $80c1 $80d1 $80e1 $c0b1 $c0c1 $c0d1 $c0e1 $100b1 $100c1 $100d1 $100e1 $140b1 $140c1 $140d1 $140e1 $17c1b $1c466 $1c47e -;.else UPDATE_BYTE $16 @ $285a $40af $40bf $40cf $40df $80af $80bf $80cf $80df $c0af $c0bf $c0cf $c0df $c0ef $100af $100bf $100cf $100df $140af $140bf $140cf $140df $17c19 $1c464 $1c47c UPDATE_BYTE $27 @ $285b $40b0 $40c0 $40d0 $40e0 $80b0 $80c0 $80d0 $80e0 $c0b0 $c0c0 $c0d0 $c0e0 $c0f0 $100b0 $100c0 $100d0 $100e0 $140b0 $140c0 $140d0 $140e0 $17c1a $1c465 $1c47d UPDATE_BYTE $18 @ $285C $40b1 $40c1 $40d1 $80e1 $80b1 $80c1 $80d1 $80e1 $c0b1 $c0c1 $c0d1 $c0e1 $100b1 $100c1 $100d1 $100e1 $140b1 $140c1 $140d1 $140e1 $17c1b $1c466 $1c47e -;.endif + ; Fire spell color. EDIT: This is regrettably not the fire spell, but a generic "after spell flash color" ; so it applies to anything that sets a spell color like grabbing a fairy or casting any spell or learning a spell, etc. ;UPDATE_BYTE $37 @ $2a0a @@ -830,28 +827,6 @@ UPDATE_BYTE $18 $10ea UPDATE_BYTE $02 @ $0e9e -; Replace writing registers directly with an output buffer to mix with the regular driver output. -; RESERVE SfxOutputBuffer, 12 - -; SND_SQUARE1_BUFFER = SfxOutputBuffer -; SND_SQUARE2_BUFFER = SfxOutputBuffer + 4 -; SND_NOISE_BUFFER = SfxOutputBuffer + 8 - -;.macro SFXInit -; -;; .if ::USE_VANILLA_SFX -; ldy #SFXMemoryEnd - SFXMemoryStart -;: sta SFXMemoryStart,y ;clear out memory used -; dey ;by the sound engines -; bpl :- -;; .endif -; -;.endmacro -; -;.macro SFXPlayback -; jsr SFXSoundEngine -;.endmacro - ;------------------------------------------------------------------------------------- .segment "PRG6" .org $9016 @@ -944,26 +919,13 @@ ElevatorMakeMarioStateStanding: sta Player_State rts -; Make mario "downstabbing" while falling -; .org $ED02 -; jsr CheckForDownstab -; bcc $ED1B -; nop ; Disable vanilla recoil when jumping off enemies .org $e747 -; jsr DisableRecoilIfDownstab jmp *+3 ; skip setting recoil for stabs SetLinkRecoil = $e371 -; .reloc -;DisableRecoilIfDownstab: -; jsr CheckForDownstab -; bcs + -; jmp SetLinkRecoil -; + -; rts ; Make mario bounce off armored enemies .org $E65E @@ -982,11 +944,11 @@ SetLinkRecoil = $e371 .org $E714 jsr CheckForDownstab bcc $E723 - LDA $0B - BNE $E723 - LDA #$fc - STA $057D - nop + lda ProjectileProcessing + bne + + lda #$fc + sta $057D ++ .assert * = $E723 .org $E9C0 diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 7e6e02458..149c3eb93 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -180,6 +180,7 @@ RESV DripperRedCounter RESV PpuCtrlForIrq RESV ScrollPosForIrq RESV IrqSetupAlready +RESV ProjectileProcessing RESV PendingAudioCall ; 1=audio running on main thread, 0=NMI decremented it (needs re-run) ; Expanded the block break list to hold up to 10 bricks at a time instead of just 5 BREAKABLE_BLOCK_COUNT = 10 From 4adfc9cb68e242d62e011e863d2b859b7afef88c Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 14 May 2026 00:17:32 -0400 Subject: [PATCH 031/128] Remove a buncha unused code --- RandomizerCore/Asm/z2mario/mario.s | 213 ----------------------------- 1 file changed, 213 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index f3023a588..3df30d489 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -2956,219 +2956,6 @@ WriteMetasprite: sta FireballMetasprite,x rts .endproc -; .proc GetXOffscreenBits -; stx R4 ;save position in buffer to here -; ldy #$01 ;start with right side of screen -; Loop: -; lda ScreenEdge_X_Pos,y ;get pixel coordinate of edge -; sec ;get difference between pixel coordinate of edge -; sbc SprObject_X_Position,x ;and pixel coordinate of object position -; sta R7 ;store here -; lda ScreenEdge_PageLoc,y ;get page location of edge -; sbc SprObject_PageLoc,x ;subtract page location of object position from it -; ldx DefaultXOnscreenOfs,y ;load offset value here -; cmp #$00 -; bmi Continue ;if beyond right edge or in front of left edge, branch -; ldx DefaultXOnscreenOfs+1,y ;if not, load alternate offset value here -; cmp #$01 -; bpl Continue ;if one page or more to the left of either edge, branch -; lda #$38 ;if no branching, load value here and store -; sta R6 -; lda #$08 ;load some other value and execute subroutine -; ; jsr DividePDiff ; inlined -; sta R5 ;store current value in A here -; lda R7 ;get pixel difference -; cmp R6 ;compare to preset value -; bcs Continue ;if pixel difference >= preset value, branch -; lsr ;divide by eight to get tile difference -; lsr -; lsr -; and #$07 ;mask out all but 3 LSB -; cpy #$01 ;right side of the screen or top? -; bcs + ;if so, branch, use difference / 8 as offset -; adc R5 ;if not, add value to difference / 8 -; + -; tax ;use as offset -; Continue: -; lda XOffscreenBitsData,x ;get bits here -; ldx R4 ;reobtain position in buffer -; cmp #$00 ;if bits not zero, branch to leave -; bne ExXOfsBS -; dey ;otherwise, do left side of screen now -; bpl Loop ;branch if not already done with left side -; ExXOfsBS: -; rts - -; XOffscreenBitsData: -; .byte $7f, $3f, $1f, $0f, $07, $03, $01, $00 -; .byte $80, $c0, $e0, $f0, $f8, $fc, $fe, $ff - -; DefaultXOnscreenOfs: -; .byte $07, $0f, $07 -; .endproc - -; .proc GetYOffscreenBits -; stx R4 ;save position in buffer to here -; ldy #$01 ;start with top of screen -; Loop: -; lda HighPosUnitData,y ;load coordinate for edge of vertical unit -; sec -; sbc SprObject_Y_Position,x ;subtract from vertical coordinate of object -; sta R7 ;store here -; lda #$01 ;subtract one from vertical high byte of object -; sbc SprObject_Y_HighPos,x -; ldx DefaultYOnscreenOfs,y ;load offset value here -; cmp #$00 -; bmi Continue ;if under top of the screen or beyond bottom, branch -; ldx DefaultYOnscreenOfs+1,y ;if not, load alternate offset value here -; cmp #$01 -; bpl Continue ;if one vertical unit or more above the screen, branch -; lda #$20 ;if no branching, load value here and store -; sta R6 -; lda #$04 ;load some other value and execute subroutine -; ; jsr DividePDiff ; inlined -; sta R5 ;store current value in A here -; lda R7 ;get pixel difference -; cmp R6 ;compare to preset value -; bcs Continue ;if pixel difference >= preset value, branch -; lsr ;divide by eight to get tile difference -; lsr -; lsr -; and #$07 ;mask out all but 3 LSB -; cpy #$01 ;right side of the screen or top? -; bcs + ;if so, branch, use difference / 8 as offset -; adc R5 ;if not, add value to difference / 8 -; + -; tax ;use as offset -; Continue: -; lda YOffscreenBitsData,x ;get offscreen data bits using offset -; ldx R4 ;reobtain position in buffer -; cmp #$00 -; bne ExYOfsBS ;if bits not zero, branch to leave -; dey ;otherwise, do bottom of the screen now -; bpl Loop -; ExYOfsBS: -; rts - -; YOffscreenBitsData: -; .byte $00, $08, $0c, $0e -; .byte $0f, $07, $03, $01 -; .byte $00 - -; DefaultYOnscreenOfs: -; .byte $04, $00, $04 - -; HighPosUnitData: -; .byte $ff, $00 - -; .endproc - -; GetOffScreenBitsSet: -; tya ;save offscreen bits offset to stack for now -; pha -; jsr GetXOffscreenBits ;do subroutine here -; lsr ;move high nybble to low -; lsr -; lsr -; lsr -; sta R0 ;store here -; jsr GetYOffscreenBits -; asl ;move low nybble to high nybble -; asl -; asl -; asl -; ora R0 ;mask together with previously saved low nybble -; sta R0 ;store both here -; pla ;get offscreen bits offset from stack -; tay -; lda R0 ;get value here and store elsewhere -; sta FBall_OffscreenBits -; ldx ObjectOffset -; rts - -; BoundingBoxCore: -; stx R0 ;save offset here - -; lda SprObject_Y_Position,x ;load vertical coordinate low -; sta R2 -; lda SprObject_X_Position,x ;load horizontal coordinate -; sec ;subtract left edge coordinate -; sbc ScreenLeft_X_Pos -; sta R1 - -; txa ;multiply offset by four and save to stack -; asl -; asl -; pha -; tay ;use as offset for Y, X is left alone - -; ;lda SprObj_BoundBoxCtrl,x ;load value here to be used as offset for X -; lda #0 -; asl ;multiply that by four and use as X -; asl -; tax -; lda R1 ;add the first number in the bounding box data to the -; clc ;relative horizontal coordinate using enemy object offset -; adc BoundBoxCtrlData,x ;and store somewhere using same offset * 4 -; sta BoundingBox_UL_Corner,y ;store here -; lda R1 -; clc -; adc BoundBoxCtrlData+2,x ;add the third number in the bounding box data to the -; sta BoundingBox_LR_Corner,y ;relative horizontal coordinate and store -; inx ;increment both offsets -; iny -; lda R2 ;add the second number to the relative vertical coordinate -; clc ;using incremented offset and store using the other -; adc BoundBoxCtrlData,x ;incremented offset -; sta BoundingBox_UL_Corner,y -; lda R2 -; clc -; adc BoundBoxCtrlData+2,x ;add the fourth number to the relative vertical coordinate -; sta BoundingBox_LR_Corner,y ;and store -; pla ;get original offset loaded into $00 * y from stack -; tay ;use as Y -; ldx R0 ;get original offset and use as X again -; rts - -; CheckRightScreenBBox: -; lda ScreenLeft_X_Pos ;add 128 pixels to left side of screen -; clc ;and store as horizontal coordinate of middle -; adc #$80 -; sta R2 -; lda ScreenLeft_PageLoc ;add carry to page location of left side of screen -; adc #$00 ;and store as page location of middle -; sta R1 -; lda SprObject_X_Position,x ;get horizontal coordinate -; cmp R2 ;compare against middle horizontal coordinate -; lda SprObject_PageLoc,x ;get page location -; sbc R1 ;subtract from middle page location -; bcc CheckLeftScreenBBox ;if object is on the left side of the screen, branch -; lda BoundingBox_DR_XPos,y ;check right-side edge of bounding box for offscreen -; bmi NoOfs ;coordinates, branch if still on the screen -; lda #$ff ;load offscreen value here to use on one or both horizontal sides -; ldx BoundingBox_UL_XPos,y ;check left-side edge of bounding box for offscreen -; bmi SORte ;coordinates, and branch if still on the screen -; sta BoundingBox_UL_XPos,y ;store offscreen value for left side -; SORte: -; sta BoundingBox_DR_XPos,y ;store offscreen value for right side -; NoOfs: -; ldx ObjectOffset ;get object offset and leave -; rts - -; CheckLeftScreenBBox: -; lda BoundingBox_UL_XPos,y ;check left-side edge of bounding box for offscreen -; bpl NoOfs2 ;coordinates, and branch if still on the screen -; cmp #$a0 ;check to see if left-side edge is in the middle of the -; bcc NoOfs2 ;screen or really offscreen, and branch if still on -; lda #$00 -; ldx BoundingBox_DR_XPos,y ;check right-side edge of bounding box for offscreen -; bpl SOLft ;coordinates, branch if still onscreen -; sta BoundingBox_DR_XPos,y ;store offscreen value for right side -; SOLft: -; sta BoundingBox_UL_XPos,y ;store offscreen value for left side -; NoOfs2: -; ldx ObjectOffset ;get object offset and leave -; rts BlockBufferChk_FBall: ldy #$1a ;set offset for block buffer adder data From 55b14a2779bda10e29280ec63813c79254fbcfc5 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 16 May 2026 01:07:11 -0400 Subject: [PATCH 032/128] SO MANY FIXES FOR EVERYTHING --- RandomizerCore/Asm/StatTracking.s | 37 ++++++++++++++- RandomizerCore/Asm/z2mario/boss.s | 58 ++++++++++++++++++++---- RandomizerCore/Asm/z2mario/integration.s | 24 +++++++--- RandomizerCore/Asm/z2mario/mario.s | 21 ++++++++- RandomizerCore/Hyrule.cs | 2 + 5 files changed, 125 insertions(+), 17 deletions(-) diff --git a/RandomizerCore/Asm/StatTracking.s b/RandomizerCore/Asm/StatTracking.s index db648a433..9973edaf1 100644 --- a/RandomizerCore/Asm/StatTracking.s +++ b/RandomizerCore/Asm/StatTracking.s @@ -49,6 +49,7 @@ SetPreviousFrameAction: sta $80 rts +.ifndef ENABLE_Z2_MARIO .org $95FC ; sty $0400 jsr StatTrackSwordSwipe .reloc @@ -114,6 +115,7 @@ StatTrackDownStab: @Exit: sta $80 rts +.endif .segment "PRG4" @@ -328,6 +330,8 @@ StopTimers: jsr AddTimestamp @AlreadyDoneOnce: ; setup and draw the old man over an over + lda #$00 + sta $c9 ; offscreen bits. $Fx means the whole block is offscreen %0000abcd - abcd are bits indicating which tile in the block is offscreen lda #$d0 sta Enemy0XPositionLo lda #$50 @@ -770,14 +774,45 @@ UpdateSpritePosition: ; link x offset lda #$08+4 sta $cc + ; link x hi byte + lda #0 + sta $3b + ; clear shield flag + sta $070F + ; link y hi byte + lda #1 + sta $19 ; link y offset lda #$20 sta LinkYPos ; link metasprite +.ifdef ENABLE_Z2_MARIO +; .import BankPatchLinkDrawRoutine, METASPRITE_BIG_MARIO_STANDING +; lda #METASPRITE_BIG_MARIO_STANDING +; sta $80 +; lda #1 +; sta CurrentPRGBank +; jmp BankPatchLinkDrawRoutine + ;This is wayyy harder than it needs to be, so i'm just gonna draw mario where i want him + ; and move on with life + ldx #$10-1 + @loop: + lda @PlayerStandingSprite,x + sta $200 + $80 + 4,x + dex + bpl @loop + rts +@PlayerStandingSprite: +.byte $30, $22, $00, $20 +.byte $30, $20, $00, $18 +.byte $20, $02, $00, $20 +.byte $20, $00, $00, $18 + +.else lda #3 sta $80 jmp $EC02 ; Draw Link based on metasprite -; rts +.endif .reloc WaitForStart: diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index 978e9ab11..74ccef33d 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -997,8 +997,6 @@ BossDrawProjectiles: @set_frame: sta EnemyProjectileType - EnemyProjectileOffset,y - ; --- Flip selection --- - ; Clear OAM flip bits while preserving palette + PROJ_TYPE_HAMMER. lda SprObject_SprAttrib,y and #%00111111 sta SprObject_SprAttrib,y @@ -1036,6 +1034,12 @@ KillBoss: sta Enemy_Y_Speed,x lda #0 sta Enemy_X_Speed,x + sta EnemyProjectileType+0 + sta EnemyProjectileType+1 + sta EnemyProjectileType+2 + sta EnemyProjectileType+3 + sta EnemyProjectileType+4 + sta EnemyProjectileType+5 lda #$7f sta boss_counter ; vanilla boss kill behavior @@ -1186,9 +1190,47 @@ MoveD_EnemyVertically: ldx #0 ;boss is always at enemy slot 0 rts -;.reloc -;MoveEnemyHorizontally: -; inx ;increment offset for enemy offset -; jsr MoveObjectHorizontally ;position object horizontally according to -; ldx ObjectOffset ;counters, return with saved value in A, -; rts ;put enemy offset back in X and leave + +; Final cutscene fixes + +.segment "PRG5" +.org $9b53 + jsr SetupMarioSpriteForCredits + nop +.reloc +SetupMarioSpriteForCredits: + lda #1 + sta $1a ; put the boss back "onscreen" for the rest of the cutscenes + lda #METASPRITE_BIG_MARIO_STANDING + sta $80 + rts + +.org $9ba8 + lda #1 ; Make mario face right when taking the triforce + +.org $9bb1 + lda #METASPRITE_BIG_MARIO_JUMPING + +; Zelda waking scene mario sprite tile fixes +.org $8c0e + .byte $14 ; Mario jumping top left +.org $8c12 + .byte $16 ; Mario jumping top right + .byte $00 ; don't flip it +.org $8c16 + .byte $34 ; Mario jumping bot left +.org $8c1a + .byte $36 ; Mario jumping bot right + .byte $00 ; dont flip it + +; Zelda kiss scene mario sprite tile fixes +.org $8d8e + .byte $04 ; top back of mario walking + .byte $20 ; not flipped +.org $8d92 + .byte $06 ; top front of mario walking +.org $8d9e + .byte $24 ; bot back of mario walking + .byte $20 ; not flipped +.org $8da2 + .byte $26 ; bot front of mario walking diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 0392dd94a..1bc0221ec 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -155,6 +155,7 @@ DrawFallingMarioSprite: .org $dd1b ; TBird draws a flashing mario sprite here without banking jsr BankPatchLinkDrawRoutine +.export BankPatchLinkDrawRoutine .reloc BankPatchLinkDrawRoutine: lda $0769 @@ -163,8 +164,9 @@ BankPatchLinkDrawRoutine: jmp @Exit + jsr SwapToPRG0 - jsr PatchLinkDrawRoutine + jsr $EC02 ; let the double return go back to here jsr SwapToSavedPRG + rts @Exit: ; double return to skip the OG drawing code pla @@ -555,7 +557,7 @@ NoDecTimers: SetPlayerDownstabbingHitbox: ; jsr $ed02 ; attempt to downstab while falling jsr CheckForDownstab - bcc + + bcc @notstabbing LDX #0 JSR $ECEA CLC @@ -565,7 +567,7 @@ SetPlayerDownstabbingHitbox: CLC ADC #$10 ; + 8 ; + 8 cause UGH STA HitboxXCoord ; ,x - + +@notstabbing: rts ; Prevent recoil and playing the sword sound when hitting a well @@ -945,11 +947,21 @@ SetLinkRecoil = $e371 jsr CheckForDownstab bcc $E723 lda ProjectileProcessing - bne + + bne @skip lda #$fc - sta $057D -+ + jsr SetDownstabStatTracking +@skip: .assert * = $E723 +.reloc +SetDownstabStatTracking: + ;set recoil to -4 to bounce up + sta $057D + ; stomped an enemy so inc stats as well + inc StatDownStabCount+0 + bne @Exit + inc StatDownStabCount+1 +@Exit: + rts .org $E9C0 jsr InyIfDownstabbing diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 3df30d489..e81a75205 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -1147,7 +1147,14 @@ ChkWtr: lda #$01 ;set value here (apparently always set to ; lda Whirlpool_Flag ;if whirlpool flag not set, branch ; beq GetYPhy ; iny ;otherwise increment to 6 -GetYPhy: lda JumpMForceData,y ;store appropriate jump/swim +GetYPhy: + + ; stat tracking the mario jump counter + inc StatUpStabCount+0 + bne @nohighinc + inc StatUpStabCount+1 +@nohighinc: + lda JumpMForceData,y ;store appropriate jump/swim sta VerticalForce ;data here lda FallMForceData,y sta VerticalForceDown @@ -2114,7 +2121,7 @@ LandPlyr: lda $cc clc adc #$10 - sta $47e + sta HitboxXCoord InitSteP: lda #$00 sta Player_State ;set player's state to normal @@ -2407,6 +2414,11 @@ ProcFireball_Bubble: lda Fireball_State,x ;load fireball state bne ProcFireballs ;if not inactive, branch SpawnFireball: +; stat tracking consider a fireball a LoStab + inc StatLoStabCount+0 + bne @Skip + inc StatLoStabCount+1 +@Skip: lda #Sfx_Fireball ;play fireball sound effect sta Square1SoundQueue lda #$02 ;load state @@ -2745,6 +2757,11 @@ GetProperObjOffset: lda Fireball_State,x ;load fireball state bne UpdateHammers ;if not inactive, branch SpawnHammer: +; stat tracking consider a hammer a HiStab + inc StatHiStabCount+0 + bne @Skip + inc StatHiStabCount+1 +@Skip: inc JustThrownHammer ; make mario use the throw animation frame ldy PlayerAnimTimerSet ;copy animation frame timer setting diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 4e548d4da..07b2293e8 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3752,6 +3752,8 @@ public void StatTracking(RandomizerProperties props, Assembler asm) a.Assign("PressStartStringLen", message.Length); AssignRealPalaceLocations(a); a.Set("_ALLOW_ITEM_DUPLICATES", props.AllowImportantItemDuplicates ? 1 : 0); + if (props.MarioMode) + a.Assign("ENABLE_Z2_MARIO", 1); a.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.StatTracking.s"), "stat_tracking.s"); } From d2a57147f26b95be75e6ea2a94303ada1ca8f581 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 16 May 2026 02:32:14 -0400 Subject: [PATCH 033/128] Re-add in an rts that is used but freed --- RandomizerCore/Asm/z2mario/integration.s | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 1bc0221ec..45ab9b28c 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -11,7 +11,7 @@ FREE "PRG0" [$92BF, $962D) ; Clear out the old flame / sword projectile code -FREE "PRG0" [$9815, $9925) +FREE "PRG0" [$9815, $9924) ; .segment "PRG0" ; .org (($10ea - $10) .mod $4000) + $8000 From cb4258e777c7e13e580573d6edfe49ab05e0720e Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 16 May 2026 02:33:03 -0400 Subject: [PATCH 034/128] Rename the fields on the stats screen --- RandomizerCore/Asm/StatTracking.s | 40 ++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/RandomizerCore/Asm/StatTracking.s b/RandomizerCore/Asm/StatTracking.s index 9973edaf1..334385102 100644 --- a/RandomizerCore/Asm/StatTracking.s +++ b/RandomizerCore/Asm/StatTracking.s @@ -1366,27 +1366,55 @@ BackgroundData: .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 .byte $f5,$ca,$cb,$cb,$cb,$cb,$cb,$cb,$cb,$cb,$cb,$cb,$ca,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 - .byte $f5,$cc,$dd,$de,$da,$ed,$e1,$ec,$cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 + .byte $f5,$cc + .byte $dd,$de,$da,$ed,$e1,$ec ; DEATHS + .byte $cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 .byte $f5,$cc,$f5,$f5,$f5,$f5,$f5,$f5,$f4,$f4,$f4,$f4,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 - .byte $f5,$cc,$eb,$de,$ec,$de,$ed,$ec,$cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 + .byte $f5,$cc + .byte $eb,$de,$ec,$de,$ed,$ec ; RESETS + .byte $cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 .byte $f5,$cc,$f5,$f5,$f5,$f5,$f5,$f5,$f4,$f4,$f4,$f4,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 - .byte $f5,$cc,$e1,$e2,$ec,$ed,$da,$db,$cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 + .byte $f5,$cc +.ifdef ENABLE_Z2_MARIO + .byte $e1,$da,$e6,$e6,$de,$eb ; HAMMER +.else + .byte $e1,$e2,$ec,$ed,$da,$db ; HISTAB +.endif + .byte $cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 .byte $f5,$cc,$f4,$f5,$f5,$f5,$f5,$f5,$f4,$f4,$f4,$f4,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 - .byte $f5,$cc,$e5,$e8,$ec,$ed,$da,$db,$cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 + .byte $f5,$cc +.ifdef ENABLE_Z2_MARIO + .byte $df,$e2,$eb,$de,$db,$e5 ; FIREBL +.else + .byte $e5,$e8,$ec,$ed,$da,$db ; LOSTAB +.endif + .byte $cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 .byte $f5,$cc,$f4,$f5,$f5,$f5,$f5,$f5,$f4,$f4,$f4,$f4,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 - .byte $f5,$cc,$ee,$e9,$ec,$ed,$da,$db,$cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 + .byte $f5,$cc +.ifdef ENABLE_Z2_MARIO + .byte $e3,$ee,$e6,$e9,$cf,$cf ; JUMP.. +.else + .byte $ee,$e9,$ec,$ed,$da,$db ; UPSTAB +.endif + .byte $cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 .byte $f5,$cc,$f4,$f5,$f5,$f5,$f5,$f5,$f4,$f4,$f4,$f4,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 - .byte $f5,$cc,$dd,$f0,$ec,$ed,$da,$db,$cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 + .byte $f5,$cc +.ifdef ENABLE_Z2_MARIO + .byte $ec,$ed,$e8,$e6,$e9,$cf ; STOMP. +.else + .byte $dd,$f0,$ec,$ed,$da,$db ; DWSTAB +.endif + .byte $cf,$d0,$d0,$d0,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 .byte $f5,$cc,$f5,$f5,$f5,$f5,$f5,$f5,$f5,$f5,$f5,$f5,$cc,$f4,$f4,$f4 .byte $f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$f4,$cc,$f5 From 8acbf5960df67bcaa8fa6827bee0fa8852127e03 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 16 May 2026 17:53:20 -0400 Subject: [PATCH 035/128] Fix second time the title screen is drawn --- RandomizerCore/Hyrule.cs | 1 + RandomizerCore/ROM.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 07b2293e8..f76e57d1f 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2419,6 +2419,7 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra } rom.UpdateSprite(props.CharSprite, true, props.ChangeItemSprites); rom.UpdateSpritePalette(props.TunicColor, props.SkinTone, props.OutlineColor, props.ShieldColor, props.BeamSprite); + // Z2Mario, when making the vanilla romhack, change this flag to false const bool isRandomized = false; if (isRandomized) rom.Put(ROM.ChrRomOffset + 0x01000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.randomizer_text.chr")); diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 0a93ebe99..bc673f3cf 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -474,7 +474,13 @@ public void AddRandomizerToTitle(Assembler asm, bool marioMode, bool isRandomize """ : """ + Line0: +; SKIP THIS IF NOT RANDOMIZED +.byte $22, $4a, 1 ; Write 1 useless byte to just skip this write +.byte $f4 + +Line1: ; ZELDA II .byte $22, $6C, 8 .byte $10, $11, $12, $13, $14, $15, $16, $17 @@ -488,11 +494,6 @@ public void AddRandomizerToTitle(Assembler asm, bool marioMode, bool isRandomize ;.byte $23,$f8,REPEAT | 7,$00 .byte $23,$ff,1,$cc -Line1: -; SKIP THIS IF NOT RANDOMIZED -.byte $22, $6a, 1 ; Write 1 useless byte to just skip this write -.byte $f4 - Line2: ; THE ADVENTURE OF TOP .byte $22, $88, 17 From 2818a21241d038abb6fdf158eb91b12bf10193d4 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 21 May 2026 18:40:09 -0400 Subject: [PATCH 036/128] Basic tanooki tail rendering --- RandomizerCore/Asm/z2mario/integration.s | 93 ++++++++++++ RandomizerCore/Asm/z2mario/mario.s | 143 +++++++++--------- RandomizerCore/Asm/z2mario/metasprite.s | 12 ++ .../Asm/z2mario/sprites_mario_tanooki.chr | Bin 0 -> 2048 bytes RandomizerCore/Asm/z2mario/tanooki_tail.chr | Bin 0 -> 160 bytes RandomizerCore/Hyrule.cs | 6 +- RandomizerCore/RandomizerCore.csproj | 4 + 7 files changed, 186 insertions(+), 72 deletions(-) create mode 100644 RandomizerCore/Asm/z2mario/sprites_mario_tanooki.chr create mode 100644 RandomizerCore/Asm/z2mario/tanooki_tail.chr diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 45ab9b28c..87d52861b 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -208,6 +208,10 @@ PatchLinkDrawRoutine: beq + jsr DrawShieldSprite + + lda $D0 ; nonzero if Tanooki is active + beq + + jsr DrawTailSprite + + ; Update the player bank if the metasprite has changed .import PlayerBankTable ldy ObjectMetasprite @@ -235,6 +239,95 @@ PatchLinkDrawRoutine: pla rts +.reloc +TailSpriteXOffset: + .byte 0, 24 +.import METASPRITE_TAIL_STAND, METASPRITE_TAIL_WALKING_2, METASPRITE_TAIL_STRAIGHT, METASPRITE_TAIL_CROUCHING, METASPRITE_TAIL_FLUTTER +TailPoseTable: + .byte METASPRITE_TAIL_STAND ; STANDING + .byte METASPRITE_TAIL_STAND ; WALKING_1 + .byte METASPRITE_TAIL_WALKING_2 ; WALKING_2 + .byte METASPRITE_TAIL_STRAIGHT ; WALKING_3 + .byte $00 ; SKIDDING early return + .byte $80 ; JUMPING special cased + .byte METASPRITE_TAIL_CROUCHING ; CROUCHING +.reloc +.proc DrawTailSprite +.import MetaspriteTableLeftLo, MetaspriteTableLeftHi +.import MetaspriteRenderLoop +.import METASPRITE_BIG_MARIO_STANDING, METASPRITE_BIG_MARIO_JUMPING +.import METASPRITE_BIG_MARIO_CROUCHING +Ptr = R0 +Atr = R3 +Xlo = R4 +Xhi = R5 +Ylo = R6 +Yhi = R7 + + lda ObjectMetasprite + sec + sbc #METASPRITE_BIG_MARIO_STANDING + bcc @noTail + cmp #METASPRITE_BIG_MARIO_CROUCHING - METASPRITE_BIG_MARIO_STANDING + 1 + bcs @noTail + + cmp #METASPRITE_BIG_MARIO_JUMPING - METASPRITE_BIG_MARIO_STANDING + beq @jumping + + tay + ldx TailPoseTable,y + bmi @jumping + bne @havePose + rts ; skidding so return early since the tail is part of the sprite +@jumping: + ; Jumping: TAIL_STRAIGHT while rising, TAIL_FLUTTER while falling. + ldx #METASPRITE_TAIL_FLUTTER + lda Player_Y_Speed + bpl @havePose + ldx #METASPRITE_TAIL_STRAIGHT +@havePose: + + lda MetaspriteTableLeftLo,x + sta Ptr + lda MetaspriteTableLeftHi,x + sta Ptr+1 + + lda PlayerFacingDir + lsr + tay + lda Player_X_Position + sec + sbc ScreenLeft_X_Pos + sta Xlo + lda SprObject_PageLoc + sbc ScreenLeft_PageLoc + sta Xhi + lda Xlo + clc + adc TailSpriteXOffset,y + sta Xlo + bcc @noinc + inc Xhi +@noinc: + lda Player_Y_HighPos + sta Yhi + lda SprObject_Y_Position + sta Ylo + + cpy #1 + bcc @flipRight + lda #%01000000 + .byte $2c +@flipRight: + lda #0 + sta Atr + + jmp MetaspriteRenderLoop + +@noTail: + rts +.endproc + .reloc ShieldSpriteXOffset: .byte 24 ; Overlaps with Y offset table to save a byte diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index e81a75205..d65b4f05c 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -897,7 +897,7 @@ MoveSubs: .word OnGroundStateSub .word JumpSwimSub .word FallingSub -.word ClimbingSub +.word $0000 ; ClimbingSub ; removed to save space NoMoveSub: rts @@ -988,56 +988,56 @@ ExitMov1: jmp MovePlayerVertically ;jump to move player vertically, then leave ;-------------------------------- -.reloc -ClimbAdderLow: - .byte $0e, $04, $fc, $f2 -ClimbAdderHigh: - .byte $00, $00, $ff, $ff - -ClimbingSub: - lda Player_YMoveForceFractional - clc ;add movement force to dummy variable - adc Player_Y_MoveForce ;save with carry - sta Player_YMoveForceFractional - ldy #$00 ;set default adder here - lda Player_Y_Speed ;get player's vertical speed - bpl MoveOnVine ;if not moving upwards, branch - dey ;otherwise set adder to $ff -MoveOnVine: sty R0 ;store adder here - adc Player_Y_Position ;add carry to player's vertical position - sta Player_Y_Position ;and store to move player up or down - lda Player_Y_HighPos - adc R0 ;add carry to player's page location - sta Player_Y_HighPos ;and store - lda Left_Right_Buttons ;compare left/right controller bits - and Player_CollisionBits ;to collision flag - beq InitCSTimer ;if not set, skip to end - ldy ClimbSideTimer ;otherwise check timer - bne ExitCSub ;if timer not expired, branch to leave - ldy #$18 - sty ClimbSideTimer ;otherwise set timer now - ldx #$00 ;set default offset here - ldy PlayerFacingDir ;get facing direction - lsr ;move right button controller bit to carry - bcs ClimbFD ;if controller right pressed, branch ahead - inx - inx ;otherwise increment offset by 2 bytes -ClimbFD: dey ;check to see if facing right - beq CSetFDir ;if so, branch, do not increment - inx ;otherwise increment by 1 byte -CSetFDir: lda Player_X_Position - clc ;add or subtract from player's horizontal position - adc ClimbAdderLow,x ;using value here as adder and X as offset - sta Player_X_Position - lda Player_PageLoc ;add or subtract carry or borrow using value here - adc ClimbAdderHigh,x ;from the player's page location - sta Player_PageLoc - lda Left_Right_Buttons ;get left/right controller bits again - eor #%00000011 ;invert them and store them while player - sta PlayerFacingDir ;is on vine to face player in opposite direction -ExitCSub: rts ;then leave -InitCSTimer: sta ClimbSideTimer ;initialize timer here - rts +; .reloc +; ClimbAdderLow: +; .byte $0e, $04, $fc, $f2 +; ClimbAdderHigh: +; .byte $00, $00, $ff, $ff + +; ClimbingSub: +; lda Player_YMoveForceFractional +; clc ;add movement force to dummy variable +; adc Player_Y_MoveForce ;save with carry +; sta Player_YMoveForceFractional +; ldy #$00 ;set default adder here +; lda Player_Y_Speed ;get player's vertical speed +; bpl MoveOnVine ;if not moving upwards, branch +; dey ;otherwise set adder to $ff +; MoveOnVine: sty R0 ;store adder here +; adc Player_Y_Position ;add carry to player's vertical position +; sta Player_Y_Position ;and store to move player up or down +; lda Player_Y_HighPos +; adc R0 ;add carry to player's page location +; sta Player_Y_HighPos ;and store +; lda Left_Right_Buttons ;compare left/right controller bits +; and Player_CollisionBits ;to collision flag +; beq InitCSTimer ;if not set, skip to end +; ldy ClimbSideTimer ;otherwise check timer +; bne ExitCSub ;if timer not expired, branch to leave +; ldy #$18 +; sty ClimbSideTimer ;otherwise set timer now +; ldx #$00 ;set default offset here +; ldy PlayerFacingDir ;get facing direction +; lsr ;move right button controller bit to carry +; bcs ClimbFD ;if controller right pressed, branch ahead +; inx +; inx ;otherwise increment offset by 2 bytes +; ClimbFD: dey ;check to see if facing right +; beq CSetFDir ;if so, branch, do not increment +; inx ;otherwise increment by 1 byte +; CSetFDir: lda Player_X_Position +; clc ;add or subtract from player's horizontal position +; adc ClimbAdderLow,x ;using value here as adder and X as offset +; sta Player_X_Position +; lda Player_PageLoc ;add or subtract carry or borrow using value here +; adc ClimbAdderHigh,x ;from the player's page location +; sta Player_PageLoc +; lda Left_Right_Buttons ;get left/right controller bits again +; eor #%00000011 ;invert them and store them while player +; sta PlayerFacingDir ;is on vine to face player in opposite direction +; ExitCSub: rts ;then leave +; InitCSTimer: sta ClimbSideTimer ;initialize timer here +; rts ;------------------------------------------------------------------------------------- ;$00 - used to store offset to friction data @@ -1071,26 +1071,27 @@ Climb_Y_MForceData: .byte $00, $20, $ff PlayerPhysicsSub: - lda Player_State ;check player state - cmp #$03 - bne CheckForJumping ;if not climbing, branch - ldy #$00 - lda Up_Down_Buttons ;get controller bits for up/down - and Player_CollisionBits ;check against player's collision detection bits - beq ProcClimb ;if not pressing up or down, branch - iny - and #%00001000 ;check for pressing up - bne ProcClimb - iny -ProcClimb: ldx Climb_Y_MForceData,y ;load value here - stx Player_Y_MoveForce ;store as vertical movement force - lda #$08 ;load default animation timing - ldx Climb_Y_SpeedData,y ;load some other value here - stx Player_Y_Speed ;store as vertical speed - bmi SetCAnim ;if climbing down, use default animation timing value - lsr ;otherwise divide timer setting by 2 -SetCAnim: sta PlayerAnimTimerSet ;store animation timer setting and leave - rts +; Remove climbing data +; lda Player_State ;check player state +; cmp #$03 +; bne CheckForJumping ;if not climbing, branch +; ldy #$00 +; lda Up_Down_Buttons ;get controller bits for up/down +; and Player_CollisionBits ;check against player's collision detection bits +; beq ProcClimb ;if not pressing up or down, branch +; iny +; and #%00001000 ;check for pressing up +; bne ProcClimb +; iny +; ProcClimb: ldx Climb_Y_MForceData,y ;load value here +; stx Player_Y_MoveForce ;store as vertical movement force +; lda #$08 ;load default animation timing +; ldx Climb_Y_SpeedData,y ;load some other value here +; stx Player_Y_Speed ;store as vertical speed +; bmi SetCAnim ;if climbing down, use default animation timing value +; lsr ;otherwise divide timer setting by 2 +; SetCAnim: sta PlayerAnimTimerSet ;store animation timer setting and leave +; rts CheckForJumping: ; ZELDA added check for on elevator diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index 50a33326f..c0582e5d2 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -104,6 +104,7 @@ .segment "PRG0" .export PlayerBankTable, MetaspriteRenderLoop, METASPRITE_MARIO_SHIELD +.export METASPRITE_TAIL_STAND, METASPRITE_TAIL_WALKING_2, METASPRITE_TAIL_STRAIGHT, METASPRITE_TAIL_CROUCHING, METASPRITE_TAIL_FLUTTER PRINT_METASPRITE_IDS = 0 @@ -758,6 +759,17 @@ MARIO_SHIELD_Y_OFFSET = -8 MARIO_SHIELD_PALETTE = $00 MetaspriteBox "MARIO", "SHIELD", $06 + +TAIL_VRAM_OFFSET = SPRITE_BANK_1 +TAIL_PALETTE = $00 +TAIL_X_OFFSET = 0 +TAIL_Y_OFFSET = 8 +MetaspriteBox "TAIL", "STRAIGHT", $56 +MetaspriteBox "TAIL", "STAND", $58 +MetaspriteBox "TAIL", "WALKING_2", $5A +MetaspriteBox "TAIL", "CROUCHING", $5C +MetaspriteBox "TAIL", "FLUTTER", $5E + ;;;;;;;;;;; ; NULL Metasprite needs to be reserved in slot 0 to allow disabling drawing a sprite before its deleted diff --git a/RandomizerCore/Asm/z2mario/sprites_mario_tanooki.chr b/RandomizerCore/Asm/z2mario/sprites_mario_tanooki.chr new file mode 100644 index 0000000000000000000000000000000000000000..3a198de248c261a6b79950f9264c227d963ac4a7 GIT binary patch literal 2048 zcmbtVL2DaF6n^RouZ+X)l0dCeqftn3bE?(^YSAuQ9s45;)hWI7u+W1=BX$=W$Suh~ zNPj{OJ><9~CGDZqKK4-3DTd}!$`qG+8c%ony_E#g*f-B%-!gCBd*7S)y*FMc#Ln&A zdxRy@w3uYwPAJl%D1^WD{wMckRhqJ*KJ_Q0OkJB9^_4cYFimLG<}b+#u8 z#f`ghw+)~0CH&6zHj$E5J)S@r#iVPB@)1!bt%WZ+{)W6c0JdfT_a<={@c|$e^zL9l z3jnkL&<6mLQZayP(vKJBtN8sTe(I&gCHP4yt=as==YxGOE#BMQymLTtR#I_jmv zIHr%^gq%d^YbnicTt?K5NbA~8du5&HMPY06>d~Lp(jtKdKN8s8olLu7VL1aqxuy2+ z$o28p0lW#%+Sbkbs||qX{Oi{TU(D@Rzw7t8s+s>g_sN?ZLD1+r(cU1K|L;XnG(W%g zWx&l^N+f+ABG)fZ)!#~GlQf^rlEfH3#Im;bSEZEx=_nka&Ko8ljglly=lYZvXD0SP zxaGC5X_1(!N`j4ANX_d5(ncUO?p!HA|dFp-hI&psJ+oS7pg|l0R7X=pn)j_^pk8 zHX#mR>#B_YIFSKAzneEM>)%+S5&*;*%d7T3daUWR?QK*5i+hfFv|ON`eh&T| zKk0VH*i`3;Ji6AVEg^c4F8|n{zcl9a>;P-YKbSW?;4KPpAwKI@>gUFXcCCIF?9+Ar zU1Bd7-jaVSP@7+Op1FE=o;CU_^uLQA@a;dZ9#E{*Uo2hxN|tMoTzlk|j*=nB6%AQ} zD_jCqNsN-h!)xH$qxl;Kq@&qXN!Px4NLKVNJ~uw#JM(9qKgQ_k>ilu-FU%i@fA0Lb wVDHQi_x{}Z;l8(9q39(^GW$--F$VC=eQ%GIoQ(RnZ4`TcH + + + + From 0ac6dc110d0f1534fd64d3fcd06185b047cc00a1 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 21 May 2026 20:16:04 -0400 Subject: [PATCH 037/128] Update banks for the sprites to account for the tanooki suit --- RandomizerCore/Asm/z2mario/integration.s | 9 +++++++++ .../Asm/z2mario/sprites_mario_tanooki.chr | Bin 2048 -> 2048 bytes RandomizerCore/Asm/z2r.inc | 2 ++ 3 files changed, 11 insertions(+) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 87d52861b..d2757f6b0 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -221,6 +221,15 @@ PatchLinkDrawRoutine: sta PlayerChrBank inc ReloadCHRBank + + ; If the tanooki suit is on, then switch to the big version + lda $d0 + beq + + lda PlayerChrBank + clc + adc #CHR_TANOOKI - CHR_BIGMARIO + sta PlayerChrBank + inc ReloadCHRBank + + @skipplayer: ; While we are here, lets draw the fireball/hammer sprites too ldy #ProjectileOffset diff --git a/RandomizerCore/Asm/z2mario/sprites_mario_tanooki.chr b/RandomizerCore/Asm/z2mario/sprites_mario_tanooki.chr index 3a198de248c261a6b79950f9264c227d963ac4a7..6fe339e0842bfb954bdd4882007110c63547a604 100644 GIT binary patch delta 168 zcmV;Z09XHj5P%S{1_Bn3A9M5Y^ziWT;6Hx+{5(AT{J?+!fRh{oDGPt!-!~^C0|8** zAd^r6H5UIrA1^O24+q1*;E%uO2L}%i195ZVlbZrG4Df$HKmYu2a5#v}|C8GSOf-D| ze?Jbuz~JmM=B~UL0002)Ix-{}00030&xZ#G51)Vk|Ni{=u;Ac7KR^Hf{=k6H%=?pD W12_vqhsZqq@c8`vc(}8k17ZYNN>Kd( delta 168 zcmZn=Xb{-I#w5lsU;O#Qmk%Eb671`0YHV!&{9yos$udlu-174B=IYAKj39mhlc|`! zy}Z7@K0iA%0|P%lJ3Bi+KQkjU)COif5_W%F?DJZbuP-;U@#JV`3+}`19yWhI{P Date: Thu, 21 May 2026 20:20:43 -0400 Subject: [PATCH 038/128] Keep tail between screens and shrink on too much damage --- RandomizerCore/Asm/z2mario/integration.s | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index d2757f6b0..aca7c6fd3 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -444,10 +444,11 @@ PlayerFirePalettePPUCommand: .org $8cf4 jsr DontClearMagicState .reloc -; prevent fire from going away in screen transitions but clear everything else +; prevent fire ($10) from going away in screen transitions but clear everything else +; also keep around the jump spell too ($02) DontClearMagicState: lda $76f - and #$10 + and #$12 sta $76f tya rts @@ -495,8 +496,9 @@ CheckIfTurningSmall: lda PlayerSize ; if player is already small don't make small bne + ; If we are fire mario, we lost the fire power, so reset our palette + ; also clear the jump spell too if we shrink lda $76f - and #~$10 + and #~$12 sta $76f jsr UpdatePlayerPalette From a077ce36ca9bddcb40a5a2d50914d9e26a4637f7 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 22 May 2026 02:01:36 -0400 Subject: [PATCH 039/128] Add tail swipe animation --- RandomizerCore/Asm/z2mario/integration.s | 77 ++++++++++++++++++++++++ RandomizerCore/Asm/z2mario/mario.s | 2 + RandomizerCore/Asm/z2mario/sfx.s | 4 ++ RandomizerCore/Asm/z2r.inc | 2 +- 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index aca7c6fd3..9caefc0a2 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -202,6 +202,11 @@ PatchLinkDrawRoutine: ldx ObjectMetasprite beq @skipplayer + ; Apply swing override if we are a tanooki. + lda PlayerFacingDir + pha + jsr ApplySwingOverrides + ldx ObjectMetasprite ; reload after override ldy #0 jsr DrawMetasprite lda $070f ; nonzero if shield is cast @@ -212,6 +217,8 @@ PatchLinkDrawRoutine: beq + jsr DrawTailSprite + + pla ; restore PlayerFacingDir + sta PlayerFacingDir ; Update the player bank if the metasprite has changed .import PlayerBankTable ldy ObjectMetasprite @@ -248,6 +255,67 @@ PatchLinkDrawRoutine: pla rts +.reloc + +.proc ProcTanookiSwing +.import METASPRITE_BIG_MARIO_CROUCHING + lda $d0 ; Tanooki suit active? + beq @done + lda tail_swing_timer + beq @maybeStart + ; count down the timer if we are already swiping + dec tail_swing_timer + rts + +@maybeStart: + lda A_B_Buttons + and #B_Button + beq @done ; B not pressed this frame + and PreviousA_B_Buttons + bne @done ; B held from last frame: not a fresh press + + lda $17 ; $17 == 0 means crouching. can't swipe while crouching! + beq @done + lda ObjectMetasprite + cmp #METASPRITE_BIG_MARIO_CROUCHING + beq @done + + lda #18 + sta tail_swing_timer +@done: + rts +.endproc + +.reloc +; Forcefully override mario's sprite when animating the tail swing +; frames 1-3 - straighten out the tail +; frames 4-7 - body forced to BIG_MARIO_STANDING until i make a new metasprite +; frames 8-11 - body STANDING, facing flipped (the "spin" frame) +; frames 12-18 - body STANDING (original facing until i make a new metasprite) +.proc ApplySwingOverrides +.import METASPRITE_BIG_MARIO_STANDING + lda $d0 + beq @done ; Tanooki not active + lda tail_swing_timer + beq @done ; not swinging + cmp #16 + bcs @done + lda #METASPRITE_BIG_MARIO_STANDING + sta ObjectMetasprite + + lda tail_swing_timer + cmp #12 + bcs @done ; past 12 frames means STANDING only + cmp #8 + bcc @done ; before 8 frames means STANDING only + ; the player is "turned around" to do the swipe + lda PlayerFacingDir + eor #%00000011 + sta PlayerFacingDir +@done: + rts +.endproc + .reloc TailSpriteXOffset: .byte 0, 24 @@ -273,6 +341,14 @@ Xhi = R5 Ylo = R6 Yhi = R7 + ; If a swing is in progress, the tail is forced to TAIL_STRAIGHT for the + ; entire animation regardless of Mario's body pose. + lda tail_swing_timer + beq @noSwing + ldx #METASPRITE_TAIL_STRAIGHT + jmp @havePose +@noSwing: + lda ObjectMetasprite sec sbc #METASPRITE_BIG_MARIO_STANDING @@ -627,6 +703,7 @@ NoDecTimers: ; This has to come before the A/B buttons are switched over jsr ProcHammerTime jsr ProcFireball_Bubble ;process fireballs and air bubbles + jsr ProcTanookiSwing lda A_B_Buttons ;save current A and B button sta PreviousA_B_Buttons ;into temp variable to be used on next frame diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index d65b4f05c..4d00817e5 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -1402,6 +1402,7 @@ OffscrJoypadBitsData: .byte $01, $02 ; ------------------------------------------------------------ +.segment "PRG0", "PRG7" .reloc ProcessPlayerAction: lda Player_State ;get player's state @@ -1581,6 +1582,7 @@ SzOfs: ExPlyrAt: rts ;leave +.segment "PRG0" ;------------------------------------------------------------------------------------- ;$00 - used for downward force ;$01 - used for upward force diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s index a83c3906f..bea1d1d87 100644 --- a/RandomizerCore/Asm/z2mario/sfx.s +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -602,3 +602,7 @@ FreqRegLookupTbl: .byte $00, $47, $00, $43, $00, $3b, $00, $35 .byte $00, $2a, $00, $23, $04, $75, $03, $57 .byte $02, $f9, $02, $cf, $01, $fc, $00, $6a +SndLev1_DataLongWag: + .byte $90, $7E, $7E, $97 ; $A5BB - $A5CA + .byte $4C, $4E, $90, $7E, $7E, $95, $52, $54, $56, $58, $94, $52, $54, $56, $58, $93 ; $A5CB - $A5DA + .byte $52, $54, $56, $58, $00 \ No newline at end of file diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index de6014fac..ded99291d 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -201,7 +201,7 @@ RESV mapMarioPrevDirection ; MARIO related values that are not directly mapped to vanilla z2 object values RESV JustThrownHammer -; RESV ObjectOffset +RESV tail_swing_timer ObjectOffset = $10 RESV FBall_OffscreenBits RESV AltEntranceControl From ab22e6d7553c3e42f70305ace6520bcfc52703db Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 22 May 2026 02:22:52 -0400 Subject: [PATCH 040/128] Port smb3 tail sfx --- RandomizerCore/Asm/z2mario/integration.s | 3 + RandomizerCore/Asm/z2mario/sfx.s | 97 +++++++++++++++++++++--- RandomizerCore/Asm/z2r.inc | 5 +- 3 files changed, 95 insertions(+), 10 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 9caefc0a2..f10db6457 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -282,6 +282,9 @@ PatchLinkDrawRoutine: lda #18 sta tail_swing_timer + lda Square1SoundQueue ; queue the tail-wag SFX on the same edge that started the swing + ora #Sfx_TailWag + sta Square1SoundQueue @done: rts .endproc diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s index bea1d1d87..8e6e31ed8 100644 --- a/RandomizerCore/Asm/z2mario/sfx.s +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -133,13 +133,90 @@ SwimStompEnvelopeData: .byte $9f, $9b, $98, $96, $95, $94, $92, $90 .byte $90, $9a, $97, $95, $93, $92 -PlayFlagpoleSlide: - lda #$40 ;store length of flagpole sound +; ----------------------------------------------------------------------------- +; SMB3 raccoon/tanooki tail-wag SFX +; +; Ported verbatim from SMB3 SndLev1_SuitLost_Cont2 prg028.asm 905-929 but +; retargeted to Square 1, since this slot replaces the dead FlagpoleSlide SFX. +; +; Data byte encoding: +; $00 - terminator +; $01-$7F - note index ($7E is "rest") +; $80-$FF - CTL1 latch, does NOT consume a new note this frame +; ----------------------------------------------------------------------------- +PlayTailWag: + lda #$00 sta Squ1_SfxLenCounter - lda #$62 ;load part of reg contents for flagpole sound - jsr SetFreq_Squ1 - ldx #$99 ;now load the rest - bne FPS2nd + lda #$90 + sta TailWag_CtlHold + ; fall through into ContinueTailWag for the first frame + +ContinueTailWag: + inc Squ1_SfxLenCounter +@fetch: + ldy Squ1_SfxLenCounter + lda SndLev1_DataLongWag-1,y + beq @streamEnd + bpl @playNote + sta TailWag_CtlHold + inc Squ1_SfxLenCounter + bne @fetch + +@playNote: + tay + lda TailWag_CtlHold + sta SND_SQUARE1_REG + lda #$7F + sta SND_SQUARE1_REG+1 + tya + jmp TailWag_PokeNote + +@streamEnd: + lda #$00 + sta Squ1_SfxLenCounter + sta Square1SoundBuffer + rts + +; Ported from SMB3 in PRG31 $E7C1 +TailWag_PokeNote: + cmp #$7E + beq @rest + pha + ldx #$01 +@octLoop: + inx + sec + sbc #24 + bpl @octLoop + clc + adc #24 + tay + lda Square1_Table_Notes,y + sta TailWag_FreqLo + lda Square1_Table_Notes+1,y + sta TailWag_FreqHi +@octShift: + lsr TailWag_FreqHi + ror TailWag_FreqLo + dex + bne @octShift + pla + cmp #56 + bcc @noAdjust + dec TailWag_FreqLo +@noAdjust: + lda TailWag_FreqLo + sta SND_SQUARE1_REG+2 + lda TailWag_FreqHi + ora #%00001000 + sta SND_SQUARE1_REG+3 +@rest: + rts + +; 12-note frequency LUT, copied verbatim from SMB3 +Square1_Table_Notes: + .word $1AB8, $1938, $17CC, $1678, $1534, $1404 + .word $12E4, $11D4, $10D4, $0FE0, $0EFC, $0E24 PlaySmallJump: lda #$26 ;branch here for small mario jumping sound @@ -212,8 +289,8 @@ Square1SfxHandler: bcs PlayFireballThrow ;fireball throw lsr Square1SoundQueue bcc + - jmp PlayFlagpoleSlide ;slide flagpole - + + jmp PlayTailWag ;tanooki tail-wag (was flagpole-slide) + + CheckSfx1Buffer: lda Square1SoundBuffer ;check for sfx in buffer @@ -232,7 +309,9 @@ CheckSfx1Buffer: lsr bcs ContinueBumpThrow ;fireball throw lsr - bcs DecrementSfx1Length ;slide flagpole + bcc + + jmp ContinueTailWag ;tanooki tail-wag (was flagpole-slide) + + ExS1H: rts PlaySwimStomp: diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index ded99291d..90c14ea01 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -290,6 +290,9 @@ RESV NoiseSoundBuffer RESV Squ1_SfxLenCounter RESV Squ2_SfxLenCounter RESV Noise_SfxLenCounter +RESV TailWag_CtlHold +RESV TailWag_FreqLo +RESV TailWag_FreqHi ; Shadow Mario Boss RAM CUSTOM_BOSS_START = RES_OFFSET @@ -423,7 +426,7 @@ Right_Dir = %00000001 ;sound effects constants Sfx_SmallJump = %10000000 -Sfx_Flagpole = %01000000 +Sfx_TailWag = %01000000 Sfx_Fireball = %00100000 Sfx_PipeDown_Injury = %00010000 Sfx_EnemySmack = %00001000 From 3228e4398cb332c309f7edd939bf302a63e52ea5 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 22 May 2026 03:48:39 -0400 Subject: [PATCH 041/128] Add tail swipe hitbox --- RandomizerCore/Asm/z2mario/integration.s | 66 +++++++++++++++++++----- RandomizerCore/Asm/z2mario/mario.s | 4 +- RandomizerCore/Asm/z2r.inc | 1 + 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index f10db6457..f137ab903 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -731,8 +731,10 @@ NoDecTimers: lda #0 sta Player_X_Speed @SkipScrollLock: + lda #$f8 + sta HitboxYCoord + jsr SetTailSwingHitbox jsr SetPlayerDownstabbingHitbox - ; jsr CheckSideviewTransition ; this is already run during collision detection jmp PlayerGfxHandler ; rts @@ -740,8 +742,17 @@ NoDecTimers: .reloc SetPlayerDownstabbingHitbox: ; jsr $ed02 ; attempt to downstab while falling - jsr CheckForDownstab + ; skip downstab hitbox on the frames we are swinging tail. + ; bit of a hack, but its *only* two frames we skip so ... whatever + lda HitboxYCoord + cmp #$f0 bcc @notstabbing + lda Player_State + cmp #1 + bne @notstabbing + lda Player_Y_Speed + bmi @notstabbing + beq @notstabbing LDX #0 JSR $ECEA CLC @@ -751,7 +762,44 @@ SetPlayerDownstabbingHitbox: CLC ADC #$10 ; + 8 ; + 8 cause UGH STA HitboxXCoord ; ,x + lda #1 + .byte $2c @notstabbing: + lda #0 + sta PlayerIsDownstabbing + rts + +; Set a hitbox for the tail swipe only on frames 9 and 12 like smb3 does it +; this gives time for the jump check to share the hitbox so we aren't as +; likely to fall through an enemy when we are trying to jump tail swipe +.reloc +SetTailSwingHitbox: + lda tail_swing_timer + beq @done + cmp #$0C + beq @active + cmp #$09 + bne @disable +@active: + lda Player_Y_Position + clc + adc #$10 + sta HitboxYCoord + lda PlayerFacingDir + cmp #1 + bne @faceLeft + lda $CC + clc + adc #30 ; 24 is the "correct" hitbox but I want to be lenient and give them some more room + jmp @writeX +@faceLeft: + lda $CC + sec + sbc #14 ; 8 is the "correct" hitbox as well, but LENIENT +@writeX: + sta HitboxXCoord +@disable: +@done: rts ; Prevent recoil and playing the sword sound when hitting a well @@ -1162,16 +1210,10 @@ InyIfDownstabbing: .reloc CheckForDownstab: - lda Player_State - cmp #1 - bne @notjumping - lda Player_Y_Speed - beq @notjumping - bmi @notjumping - sec - rts -@notjumping: - clc + ; i broke this out into its own function cause originally it was much longer + ; 1 if downstabbing 0 otherwise + lda PlayerIsDownstabbing + lsr rts .segment "PRG3" diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 4d00817e5..dcba3b185 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -951,8 +951,8 @@ JumpSwimSub: bcc ProcSwim ;or just starting to jump, if just starting, skip ahead DumpFall: ; Force downstab hitbox?? - lda Player_Y_Position - sta HitboxYCoord + ; lda Player_Y_Position + ; sta HitboxYCoord lda VerticalForceDown ;otherwise dump falling into main fractional sta VerticalForce ProcSwim: diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 90c14ea01..8bef9156e 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -250,6 +250,7 @@ RESV Player_Pos_ForScroll RESV Player_X_Scroll RESV Player_X_MoveForce RESV PlayerStandingMetatile ; added to keep this for door checks and other metatiles +RESV PlayerIsDownstabbing ; Simplify downstab checks by just storing if we set the hitbox ; Non-timer timer. RESV FireballThrowingTimer From 6456f47d11d8fd47696671bac118d804f4d7a59f Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 22 May 2026 10:51:31 -0400 Subject: [PATCH 042/128] Add a brick break hitbox to the tail --- RandomizerCore/Asm/z2mario/integration.s | 28 ++++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index f137ab903..7499c0d5f 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -795,7 +795,9 @@ SetTailSwingHitbox: @faceLeft: lda $CC sec - sbc #14 ; 8 is the "correct" hitbox as well, but LENIENT + sbc #10 ; 0 is the "correct" hitbox I THINK? + ; these numbers were tested so that mario breaks the brick next to him with his + ; tail, and not the brick past the brick next to him. @writeX: sta HitboxXCoord @disable: @@ -820,21 +822,19 @@ bank7_CollisionTest = $E9F9 jsr CheckHammerHitboxes .reloc CheckHammerHitboxes: + ; Check if the stab is hitting a brick before hammers lda #0 sta $0b - bit Fireball_State+1 - bvc + - ldy #1 - jsr LoadProjectileCollisionBox - lda R0 - sta HitboxXCoord - lda R1 - sta HitboxYCoord - jsr BreakBlockCollisionCheck - + - bit Fireball_State - bvc + - ldy #0 + ; Run the check for tail hitbox (also stomp someday??) + jsr BreakBlockCollisionCheck + ; check one hammer every other frame instead of both. + ; its good enough in practice + lda FrameCounter + and #1 + tay + lda Fireball_State,y + and #$40 + beq + jsr LoadProjectileCollisionBox lda R0 sta HitboxXCoord From 16a9a0cff7147da231dbb4b2bff958b93adcc2a1 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 22 May 2026 14:59:17 -0400 Subject: [PATCH 043/128] Fix tail swipe hitbox and animations --- RandomizerCore/Asm/z2mario/integration.s | 98 ++++++++++++++++++++---- RandomizerCore/Asm/z2mario/mario.s | 3 +- RandomizerCore/Asm/z2mario/metasprite.s | 4 +- RandomizerCore/Asm/z2r.inc | 10 ++- 4 files changed, 91 insertions(+), 24 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 7499c0d5f..1742b6d4b 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -209,16 +209,16 @@ PatchLinkDrawRoutine: ldx ObjectMetasprite ; reload after override ldy #0 jsr DrawMetasprite - lda $070f ; nonzero if shield is cast - beq + - jsr DrawShieldSprite - + lda $D0 ; nonzero if Tanooki is active beq + jsr DrawTailSprite + pla ; restore PlayerFacingDir sta PlayerFacingDir + lda $070f ; nonzero if shield is cast + beq + + jsr DrawShieldSprite + + ; Update the player bank if the metasprite has changed .import PlayerBankTable ldy ObjectMetasprite @@ -232,6 +232,8 @@ PatchLinkDrawRoutine: lda $d0 beq + lda PlayerChrBank + cmp #CHR_BIGMARIO + bne + clc adc #CHR_TANOOKI - CHR_BIGMARIO sta PlayerChrBank @@ -259,20 +261,55 @@ PatchLinkDrawRoutine: .proc ProcTanookiSwing .import METASPRITE_BIG_MARIO_CROUCHING +TANOOKI_FLUTTER_CAP = 1 ; SMB3 PLAYER_TAILWAG_YVEL +FLUTTER_DURATION = 16 ; SMB3 Player_WagCount initial value lda $d0 ; Tanooki suit active? beq @done - lda tail_swing_timer + + lda TailWagCount + beq @afterFlutter + dec TailWagCount + lda Player_Y_Speed + bmi @afterFlutter ; rising: never cap + cmp #TANOOKI_FLUTTER_CAP + bcc @afterFlutter ; already below cap + lda #TANOOKI_FLUTTER_CAP + sta Player_Y_Speed +@afterFlutter: + + ; Check if we are starting a flutter + ; Skip if we just jumped this frame + lda Square1SoundQueue + and #Sfx_BigJump + bne @noAPress + lda A_B_Buttons + and #A_Button + beq @noAPress + and PreviousA_B_Buttons + bne @noAPress + lda #FLUTTER_DURATION + sta TailWagCount + ; Only play the sfx after it ends though to match smb3 + lda Square1SoundBuffer + and #Sfx_TailWag + bne @noAPress + lda Square1SoundQueue + ora #Sfx_TailWag + sta Square1SoundQueue +@noAPress: + + lda TailSwingTimer beq @maybeStart ; count down the timer if we are already swiping - dec tail_swing_timer + dec TailSwingTimer rts @maybeStart: lda A_B_Buttons and #B_Button - beq @done ; B not pressed this frame + beq @done and PreviousA_B_Buttons - bne @done ; B held from last frame: not a fresh press + bne @done lda $17 ; $17 == 0 means crouching. can't swipe while crouching! beq @done @@ -281,8 +318,8 @@ PatchLinkDrawRoutine: beq @done lda #18 - sta tail_swing_timer - lda Square1SoundQueue ; queue the tail-wag SFX on the same edge that started the swing + sta TailSwingTimer + lda Square1SoundQueue ; queue the tail-wag SFX on swing start ora #Sfx_TailWag sta Square1SoundQueue @done: @@ -299,14 +336,14 @@ PatchLinkDrawRoutine: .import METASPRITE_BIG_MARIO_STANDING lda $d0 beq @done ; Tanooki not active - lda tail_swing_timer + lda TailSwingTimer beq @done ; not swinging cmp #16 bcs @done lda #METASPRITE_BIG_MARIO_STANDING sta ObjectMetasprite - lda tail_swing_timer + lda TailSwingTimer cmp #12 bcs @done ; past 12 frames means STANDING only cmp #8 @@ -346,7 +383,7 @@ Yhi = R7 ; If a swing is in progress, the tail is forced to TAIL_STRAIGHT for the ; entire animation regardless of Mario's body pose. - lda tail_swing_timer + lda TailSwingTimer beq @noSwing ldx #METASPRITE_TAIL_STRAIGHT jmp @havePose @@ -366,13 +403,33 @@ Yhi = R7 ldx TailPoseTable,y bmi @jumping bne @havePose +@noTail: rts ; skidding so return early since the tail is part of the sprite @jumping: + ; Flutter animation override: + ; wag_count 15..13 (frames 2-4) -> TAIL_CROUCHING + ; wag_count 12..10 (frames 5-7) -> TAIL_STRAIGHT + ; otherwise -> default jumping pose + lda TailWagCount + beq @jumpDefault + cmp #16 + bcs @jumpDefault ; frame 1 (just-pressed): no override yet + cmp #13 + bcs @flutterCrouching ; 13..15 + cmp #10 + bcs @flutterStraight ; 10..12 +@jumpDefault: ; Jumping: TAIL_STRAIGHT while rising, TAIL_FLUTTER while falling. ldx #METASPRITE_TAIL_FLUTTER lda Player_Y_Speed bpl @havePose ldx #METASPRITE_TAIL_STRAIGHT + jmp @havePose +@flutterCrouching: + ldx #METASPRITE_TAIL_CROUCHING + jmp @havePose +@flutterStraight: + ldx #METASPRITE_TAIL_STRAIGHT @havePose: lda MetaspriteTableLeftLo,x @@ -412,8 +469,6 @@ Yhi = R7 jmp MetaspriteRenderLoop -@noTail: - rts .endproc .reloc @@ -532,6 +587,11 @@ DontClearMagicState: tya rts +; Don't clear out the A_B_Buttons in loading +; keeps mario from firing a fireball on screen load +.org $90b5 + jmp *+3 + ; patch end of link's loading routine .org $90DB ; sta $69DE jsr SetupMarioControl @@ -593,7 +653,11 @@ CheckIfTurningSmall: ; sta PlayerChangeSizeFlag lda #0 sta ScrollAmount - ; sta PlayerSize + ; kill all current projectiles since we can't hammer or fire while small + sta Fireball_State+0 + sta Fireball_State+1 + sta FireballMetasprite+0 + sta FireballMetasprite+1 lda #$ff sta TimerControl ;set master timer control flag to halt timers lda #Sfx_PipeDown_Injury @@ -774,7 +838,7 @@ SetPlayerDownstabbingHitbox: ; likely to fall through an enemy when we are trying to jump tail swipe .reloc SetTailSwingHitbox: - lda tail_swing_timer + lda TailSwingTimer beq @done cmp #$0C beq @active diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index dcba3b185..9cff09f49 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -2453,7 +2453,8 @@ FireballObjCore: dey ;if fireball state set to 1, skip this part and just run it beq RunFB lda Player_X_Position ;get player's horizontal position - adc #$04 ;add four pixels and store as fireball's horizontal position + ; adc #$04 ;add four pixels and store as fireball's horizontal position + adc #12 ; z2mario: Add an extra 8 px to account for hitbox jank sta Fireball_X_Position,x lda Player_PageLoc ;get player's page location adc #$00 ;add carry and store as fireball's page location diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index c0582e5d2..eeaeecf83 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -730,11 +730,11 @@ LAST_MARIO_METASPRITE = METASPRITES_COUNT FIREBALL_VRAM_OFFSET = SPRITE_BANK_0 FIREBALL_PALETTE = $01 -FIREBALL_X_OFFSET = 8 +FIREBALL_X_OFFSET = 0 FIREBALL_Y_OFFSET = -12 EXPLOSION_VRAM_OFFSET = SPRITE_BANK_1 EXPLOSION_PALETTE = $00 -EXPLOSION_X_OFFSET = 8 +EXPLOSION_X_OFFSET = 0 EXPLOSION_Y_OFFSET = -12 ; Vanilla offsets the explosion by -4. The other -8 is to account for position due to 8x16 sprites? MetaspriteBox "FIREBALL", "FRAME_1", $3c MetaspriteBox "FIREBALL", "FRAME_2", $3e diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 8bef9156e..7496fd097 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -201,7 +201,9 @@ RESV mapMarioPrevDirection ; MARIO related values that are not directly mapped to vanilla z2 object values RESV JustThrownHammer -RESV tail_swing_timer +RESV TailSwingTimer +; SMB3 raccoon flutter: nonzero = Y-velocity cap active +RESV TailWagCount ObjectOffset = $10 RESV FBall_OffscreenBits RESV AltEntranceControl @@ -282,9 +284,9 @@ RESV PlayerProj_Cnt, 2 ; Values use for SMB1 sfx audio engine RESV Sfx_SecondaryCounter -RESV PauseSoundBuffer -RESV PauseModeFlag -RESV PauseSoundQueue +; RESV PauseSoundBuffer +; RESV PauseModeFlag +; RESV PauseSoundQueue RESV Square1SoundBuffer RESV Square2SoundBuffer RESV NoiseSoundBuffer From 643e4131e33c5c9feee679c1151426353a954a8f Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 23 May 2026 01:25:17 -0400 Subject: [PATCH 044/128] Various fixes --- RandomizerCore/Asm/z2mario/integration.s | 72 +++- RandomizerCore/Asm/z2mario/mario.s | 424 +++++------------------ RandomizerCore/Asm/z2r.inc | 2 +- RandomizerCore/Hyrule.cs | 9 +- 4 files changed, 148 insertions(+), 359 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 1742b6d4b..d9e331347 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -13,10 +13,6 @@ FREE "PRG0" [$92BF, $962D) ; Clear out the old flame / sword projectile code FREE "PRG0" [$9815, $9924) -; .segment "PRG0" -; .org (($10ea - $10) .mod $4000) + $8000 -; .byte $17 - .segment "PRG7" SpellCastingRoutine = $8DC3 ; Link Main .org $D3EC ; patches the main sideview routine right before checking marios code @@ -127,6 +123,22 @@ PatchLinkDeathSprite: jsr SwapCHR jmp BankSwitchMarioCHR +.org $8DDC + jsr PreventTanookiSpellWhenSmallMario +.reloc +PreventTanookiSpellWhenSmallMario: + ldy $0749 ; original instruction we replaced + cpy #1 + bne @allow + lda PlayerSize + beq @allow + ; double return if we are small to disallow casting jump while small + pla + pla + jmp $8E1F +@allow: + rts + .org $8fe3 jmp *+3 ; Skip duplicate draw??? .reloc @@ -534,6 +546,8 @@ FacingRight: .reloc OverworldLateInit: sta $0736 ; Sets game mode + lda #$18 ; player palette + sta $69de jmp UpdatePlayerPalette .org $8D61 @@ -565,6 +579,13 @@ UpdatePlayerPalette: clc adc #6 sta $301 + ; Check if we are still using shield. + lda $69de + cmp #$18 ; Check if the current "user" color is the same as the original color + beq + + ; if not, then replace it (this is the right address trust me) + sta $301,y ++ pla tax rts @@ -585,7 +606,7 @@ DontClearMagicState: and #$12 sta $76f tya - rts + jmp UpdatePlayerPalette ; Don't clear out the A_B_Buttons in loading ; keeps mario from firing a fireball on screen load @@ -597,14 +618,14 @@ DontClearMagicState: jsr SetupMarioControl .reloc SetupMarioControl: -; sta $69DE ; This is the color index 3 for the firey palette + sta $69DE ; This is the color index 3 for the firey palette lda #8 sta GameEngineSubroutine lda #1 ; Ground area type sta AreaType lda #$28 ;store value here sta VerticalForceDown ;for fractional movement downwards if necessary - rts + jmp UpdatePlayerPalette ; Patch link's standard routine @@ -1118,9 +1139,8 @@ UPDATE_BYTE $16 @ $2a0a UPDATE_BYTE $27 @ $2a10 UPDATE_BYTE $18 @ $2a16 - ; regular palette color location -UPDATE_BYTE $18 $10ea +UPDATE_BYTE $18 @ $10ea ; shield spell color location UPDATE_BYTE $02 @ $0e9e @@ -1217,12 +1237,21 @@ ElevatorMakeMarioStateStanding: sta Player_State rts +SetObjectRecoil = $e371 ; Disable vanilla recoil when jumping off enemies .org $e747 - jmp *+3 ; skip setting recoil for stabs - -SetLinkRecoil = $e371 + jsr SkipRecoilForStabs ; skip setting recoil for stabs +.reloc +SkipRecoilForStabs: + jsr CheckForDownstab + bcs + + lda ProjectileProcessing + bne + + ; not stabbing or tossing a hammer so add recoil to the current object + jmp SetObjectRecoil + + + rts ; Make mario bounce off armored enemies @@ -1238,16 +1267,29 @@ SetLinkRecoil = $e371 .org $E66A rts +.org $e395 + jsr SkipEnemyRecoilIfThrowingHammer +.reloc +SkipEnemyRecoilIfThrowingHammer: + ; if we hit an enemy through a hammer, then we don't want to set the recoil + ldy ProjectileProcessing + beq + + lda #0 + + + sta $043e,x ; remove the recoil if it was hit by a hammer + rts + ; Make mario bounce off armored enemies .org $E714 - jsr CheckForDownstab - bcc $E723 lda ProjectileProcessing bne @skip - lda #$fc + jsr CheckForDownstab + bcc @skip + lda #$fc ; set player recoil to -4 jsr SetDownstabStatTracking @skip: .assert * = $E723 + .reloc SetDownstabStatTracking: ;set recoil to -4 to bounce up diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 9cff09f49..ec340eb9f 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -47,14 +47,14 @@ lda GameEngineSubroutine ;run routine based on number (a few of these routines are jsr JumpEngine ;merely placeholders as conditions for other routines) - .word Entrance_GameTimerSetup - .word Vine_AutoClimb - .word SideExitPipeEntry - .word VerticalPipeEntry - .word FlagpoleSlide - .word PlayerEndLevel + .word $0000 ; Entrance_GameTimerSetup + .word $0000 ; Vine_AutoClimb + .word $0000 ; SideExitPipeEntry + .word $0000 ; VerticalPipeEntry + .word $0000 ; FlagpoleSlide + .word $0000 ; PlayerEndLevel .word PlayerLoseLife - .word PlayerEntrance + .word $0000 ; PlayerEntrance .word PlayerCtrlRoutine .word PlayerChangeSize .word PlayerInjuryBlink @@ -63,82 +63,82 @@ .endproc -;------------------------------------------------------------------------------------- -; ; .reloc -PlayerEntrance: - lda AltEntranceControl ;check for mode of alternate entry - cmp #$02 - beq EntrMode2 ;if found, branch to enter from pipe or with vine - lda #$00 - ldy Player_Y_Position ;if vertical position above a certain - cpy #$30 ;point, nullify controller bits and continue - bcc AutoControlPlayer ;with player movement code, do not return - lda PlayerEntranceCtrl ;check player entry bits from header - cmp #$06 - beq ChkBehPipe ;if set to 6 or 7, execute pipe intro code - cmp #$07 ;otherwise branch to normal entry - bne PlayerRdy -ChkBehPipe: - ; lda InPipeTransition - lda Player_SprAttrib ;check for sprite attributes - bne IntroEntr ;branch if found - lda #$01 - jmp AutoControlPlayer ;force player to walk to the right -IntroEntr: - jsr EnterSidePipe ;execute sub to move player to the right - dec ChangeAreaTimer ;decrement timer for change of area - bne ExitEntr ;branch to exit if not yet expired - ; inc DisableIntermediate ;set flag to skip world and lives display - ; jmp NextArea ;jump to increment to next area and set modes -EntrMode2: - lda JoypadOverride ;if controller override bits set here, - bne VineEntr ;branch to enter with vine - ; lda #3 - ; jsr SetupPipeTransitionOverlay - lda #$ff ;otherwise, set value here then execute sub - jsr MovePlayerYAxis ;to move player upwards - lda Player_Y_Position ;check to see if player is at a specific coordinate - cmp #$91 ;if player risen to a certain point (this requires pipes - bcs @ContinuePipeEntry ;to be at specific height to look/function right) branch - ; .import FRAME_LAG_COUNT - ; lda #FRAME_LAG_COUNT - ; lda #0 - ; sta PipeExitTimer - ; jsr SetupPipeTransitionOverlay - jmp PlayerRdy -@ContinuePipeEntry: - rts ;to the last part, otherwise leave -VineEntr: - lda Vine_Height - cmp #$60 ;check vine height - bne ExitEntr ;if vine not yet reached maximum height, branch to leave - lda Player_Y_Position ;get player's vertical coordinate - cmp #$99 ;check player's vertical coordinate against preset value - ldy #$00 ;load default values to be written to - lda #$01 ;this value moves player to the right off the vine - bcc OffVine ;if vertical coordinate < preset value, use defaults - lda #$03 - sta Player_State ;otherwise set player state to climbing - iny ;increment value in Y - ; lda #$08 ;set block in block buffer to cover hole, then - ; sta Block_Buffer_1+$b4 ;use same value to force player to climb -OffVine: - sty DisableCollisionDet ;set collision detection disable flag - jsr AutoControlPlayer ;use contents of A to move player up or right, execute sub - lda Player_X_Position - cmp #$48 ;check player's horizontal position - bcc ExitEntr ;if not far enough to the right, branch to leave -PlayerRdy: - lda #$08 ;set routine to be executed by game engine next frame - sta GameEngineSubroutine - lda #$01 ;set to face player to the right - sta PlayerFacingDir - lsr ;init A - sta AltEntranceControl ;init mode of entry - sta DisableCollisionDet ;init collision detection disable flag - sta JoypadOverride ;nullify controller override bits -ExitEntr: - rts ;leave! +; ;------------------------------------------------------------------------------------- +; ; ; .reloc +; PlayerEntrance: +; lda AltEntranceControl ;check for mode of alternate entry +; cmp #$02 +; beq EntrMode2 ;if found, branch to enter from pipe or with vine +; lda #$00 +; ldy Player_Y_Position ;if vertical position above a certain +; cpy #$30 ;point, nullify controller bits and continue +; bcc AutoControlPlayer ;with player movement code, do not return +; lda PlayerEntranceCtrl ;check player entry bits from header +; cmp #$06 +; beq ChkBehPipe ;if set to 6 or 7, execute pipe intro code +; cmp #$07 ;otherwise branch to normal entry +; bne PlayerRdy +; ChkBehPipe: +; ; lda InPipeTransition +; lda Player_SprAttrib ;check for sprite attributes +; bne IntroEntr ;branch if found +; lda #$01 +; jmp AutoControlPlayer ;force player to walk to the right +; IntroEntr: +; jsr EnterSidePipe ;execute sub to move player to the right +; dec ChangeAreaTimer ;decrement timer for change of area +; bne ExitEntr ;branch to exit if not yet expired +; ; inc DisableIntermediate ;set flag to skip world and lives display +; ; jmp NextArea ;jump to increment to next area and set modes +; EntrMode2: +; lda JoypadOverride ;if controller override bits set here, +; bne VineEntr ;branch to enter with vine +; ; lda #3 +; ; jsr SetupPipeTransitionOverlay +; lda #$ff ;otherwise, set value here then execute sub +; jsr MovePlayerYAxis ;to move player upwards +; lda Player_Y_Position ;check to see if player is at a specific coordinate +; cmp #$91 ;if player risen to a certain point (this requires pipes +; bcs @ContinuePipeEntry ;to be at specific height to look/function right) branch +; ; .import FRAME_LAG_COUNT +; ; lda #FRAME_LAG_COUNT +; ; lda #0 +; ; sta PipeExitTimer +; ; jsr SetupPipeTransitionOverlay +; jmp PlayerRdy +; @ContinuePipeEntry: +; rts ;to the last part, otherwise leave +; VineEntr: +; lda Vine_Height +; cmp #$60 ;check vine height +; bne ExitEntr ;if vine not yet reached maximum height, branch to leave +; lda Player_Y_Position ;get player's vertical coordinate +; cmp #$99 ;check player's vertical coordinate against preset value +; ldy #$00 ;load default values to be written to +; lda #$01 ;this value moves player to the right off the vine +; bcc OffVine ;if vertical coordinate < preset value, use defaults +; lda #$03 +; sta Player_State ;otherwise set player state to climbing +; iny ;increment value in Y +; ; lda #$08 ;set block in block buffer to cover hole, then +; ; sta Block_Buffer_1+$b4 ;use same value to force player to climb +; OffVine: +; sty DisableCollisionDet ;set collision detection disable flag +; jsr AutoControlPlayer ;use contents of A to move player up or right, execute sub +; lda Player_X_Position +; cmp #$48 ;check player's horizontal position +; bcc ExitEntr ;if not far enough to the right, branch to leave +; PlayerRdy: +; lda #$08 ;set routine to be executed by game engine next frame +; sta GameEngineSubroutine +; lda #$01 ;set to face player to the right +; sta PlayerFacingDir +; lsr ;init A +; sta AltEntranceControl ;init mode of entry +; sta DisableCollisionDet ;init collision detection disable flag +; sta JoypadOverride ;nullify controller override bits +; ExitEntr: +; rts ;leave! ;------------------------------------------------------------------------------------- @@ -292,97 +292,6 @@ PlayerSubs: ; inc AltEntranceControl ;set mode of entry to 3 rts -;------------------------------------------------------------------------------------- -; .reloc -Vine_AutoClimb: - lda Player_Y_HighPos ;check to see whether player reached position - bne AutoClimb ;above the status bar yet and if so, set modes - lda Player_Y_Position - cmp #$e4 - bcc SetEntr -AutoClimb: - lda #%00001000 ;set controller bits override to up - sta JoypadOverride - ldy #$03 ;set player state to climbing - sty Player_State - jmp AutoControlPlayer -SetEntr: - lda #$02 ;set starting position to override - sta AltEntranceControl - jmp ChgAreaMode ;set modes - -;------------------------------------------------------------------------------------- -; .reloc - -VerticalPipeEntry: - lda #$01 ;set 1 as movement amount - jsr MovePlayerYAxis ;do sub to move player downwards - ; jsr ScrollHandler ;do sub to scroll screen with saved force if necessary - ldy #$00 ;load default mode of entry - lda WarpZoneControl ;check warp zone control variable/flag - bne ChgAreaPipe ;if set, branch to use mode 0 - iny - lda AreaType ;check for castle level type - cmp #$03 - bne ChgAreaPipe ;if not castle type level, use mode 1 - iny - jmp ChgAreaPipe ;otherwise use mode 2 - -MovePlayerYAxis: - clc - adc Player_Y_Position ;add contents of A to player position - sta Player_Y_Position - rts - -;------------------------------------------------------------------------------------- -; .reloc - -SideExitPipeEntry: - jsr EnterSidePipe ;execute sub to move player to the right - ldy #$02 -ChgAreaPipe: dec ChangeAreaTimer ;decrement timer for change of area - bne ExitCAPipe - sty AltEntranceControl ;when timer expires set mode of alternate entry -ChgAreaMode: - ; inc DisableScreenFlag ;set flag to disable screen output - ; lda #$00 - ; sta OperMode_Task ;set secondary mode of operation - ; sta Sprite0HitDetectFlag ;disable sprite 0 check - ; jsr SetupPipeTransitionOverlay - ; lda #0 ; make sure we have the value 0 set since its used right after -ExitCAPipe: rts ;leave - -EnterSidePipe: - lda #$08 ;set player's horizontal speed - sta Player_X_Speed -; lda InPipeTransition -; bne :+ -; lda #2 -; jsr SetupPipeTransitionOverlay -; : - ldy #$01 ;set controller right button by default - lda Player_X_Position ;mask out higher nybble of player's - and #%00001111 ;horizontal position - bne RightPipe - sta Player_X_Speed ;if lower nybble = 0, set as horizontal speed - tay ;and nullify controller bit override here -RightPipe: - tya ;use contents of Y to - jmp AutoControlPlayer ;execute player control routine with ctrl bits nulled - -;------------------------------------------------------------------------------------- -; .reloc -;page numbers are in order from -1 to -4 -; HalfwayPageNybbles: -; .byte $56, $40 -; .byte $65, $70 -; .byte $66, $40 -; .byte $66, $40 -; .byte $66, $40 -; .byte $66, $60 -; .byte $65, $70 -; .byte $00, $00 - .proc PlayerLoseLife ; inc DisableScreenFlag ;disable screen and sprite 0 check ; lda #$00 @@ -428,87 +337,6 @@ RightPipe: ; jmp RunGameOver::ContinueGame ;continue the game .endproc -; .reloc - -PlayerStarting_X_Pos: - .byte $28, $18 - .byte $38, $28 - -AltYPosOffset: - .byte $08, $00 - -PlayerStarting_Y_Pos: - .byte $00, $20, $b0, $50, $00, $00, $b0, $b0 - .byte $f0 - -PlayerBGPriorityData: - .byte $00, $20, $00, $00, $00, $00, $00, $00 - -GameTimerData: - .byte $20 ;dummy byte, used as part of bg priority data - .byte $04, $03, $02 - -Entrance_GameTimerSetup: - lda ScreenLeft_PageLoc ;set current page for area objects - sta Player_PageLoc ;as page location for player - lda #$28 ;store value here - sta VerticalForceDown ;for fractional movement downwards if necessary - lda #$01 ;set high byte of player position and - sta PlayerFacingDir ;set facing direction so that player faces right - sta Player_Y_HighPos - lda #$00 ;set player state to on the ground by default - sta Player_State - sta Player_CollisionBits ;initialize player's collision bits - ldy #$00 ;initialize halfway page - sty HalfwayPage - lda AreaType ;check area type - bne ChkStPos ;if water type, set swimming flag, otherwise do not set - iny -ChkStPos: sty SwimmingFlag - ldx PlayerEntranceCtrl ;get starting position loaded from header - ldy AltEntranceControl ;check alternate mode of entry flag for 0 or 1 - beq SetStPos - cpy #$01 - beq SetStPos - ldx AltYPosOffset-2,y ;if not 0 or 1, override $0710 with new offset in X -SetStPos: lda PlayerStarting_X_Pos,y ;load appropriate horizontal position - sta Player_X_Position ;and vertical positions for the player, using - lda PlayerStarting_Y_Pos,x ;AltEntranceControl as offset for horizontal and either $0710 - sta Player_Y_Position ;or value that overwrote $0710 as offset for vertical - lda PlayerBGPriorityData,x - sta Player_SprAttrib ;set player sprite attributes using offset in X - jsr GetPlayerColors ;get appropriate player palette - ; ldy GameTimerSetting ;get timer control value from header - ; beq ChkOverR ;if set to zero, branch (do not use dummy byte for this) - ; lda FetchNewGameTimerFlag ;do we need to set the game timer? if not, use - ; beq ChkOverR ;old game timer setting - ; lda GameTimerData,y ;if game timer is set and game timer flag is also set, - ; sta GameTimerDisplay ;use value of game timer control for first digit of game timer - ; lda #$01 - ; sta GameTimerDisplay+2 ;set last digit of game timer to 1 - ; lsr - ; sta GameTimerDisplay+1 ;set second digit of game timer - ; sta FetchNewGameTimerFlag ;clear flag for game timer reset - ; sta StarInvincibleTimer ;clear star mario timer -ChkOverR: ldy JoypadOverride ;if controller bits not set, branch to skip this part - beq ChkSwimE - ; lda #$03 ;set player state to climbing - ; sta Player_State - ; ldx #$00 ;set offset for first slot, for block object - ; jsr InitBlock_XY_Pos - ; lda #$f0 ;set vertical coordinate for block object - ; sta Block_Y_Position - ; ldx #$05 ;set offset in X for last enemy object buffer slot - ; ldy #$00 ;set offset in Y for object coordinates used earlier - ; farcall Setup_Vine ;do a sub to grow vine -ChkSwimE: ldy AreaType ;if level not water-type, - bne SetPESub ;skip this subroutine - ; farcall SetupBubble ;otherwise, execute sub to set up air bubbles -SetPESub: lda #$07 ;set to run player entrance subroutine - sta GameEngineSubroutine ;on the next frame of game engine - rts - - ;------------------------------------------------------------------------------------- ;$00 - used to store player's vertical offscreen bits ; .reloc @@ -775,92 +603,6 @@ ResetPalStar: sta Player_SprAttrib ;store as new player attributes rts ;and leave -;------------------------------------------------------------------------------------- -.reloc -FlagpoleSlide: - ; lda Enemy_ID+5 ;check special use enemy slot - ; cmp #FlagpoleFlagObject ;for flagpole flag object - ; bne NoFPObj ;if not found, branch to something residual - ; lda FlagpoleSoundQueue ;load flagpole sound - ; sta Square1SoundQueue ;into square 1's sfx queue - ; lda #$00 - ; sta FlagpoleSoundQueue ;init flagpole sound queue - ldy Player_Y_Position - cpy #$9e ;check to see if player has slid down - bcs SlidePlayer ;far enough, and if so, branch with no controller bits set - lda #$04 ;otherwise force player to climb down (to slide) -SlidePlayer: - jmp AutoControlPlayer ;jump to player control routine -; NoFPObj: inc GameEngineSubroutine ;increment to next routine (this may -; rts ;be residual code) - -;------------------------------------------------------------------------------------- -.reloc -; Hidden1UpCoinAmts: -; .byte $15, $23, $16, $1b, $17, $18, $23, $63 - -PlayerEndLevel: - lda #$01 ;force player to walk to the right - jsr AutoControlPlayer - lda Player_Y_Position ;check player's vertical position - cmp #$ae - bcc ChkStop ;if player is not yet off the flagpole, skip this part -; .if ::USE_SMB2J_FEATURES -; lda #$00 -; sta ScrollLock ;reactivate scroll -; lda FlagpoleMusicFlag ;check flag to see if music was already queued -; bne ChkStop ;if so, skip this -; lda #EndOfLevelMusic -; sta EventMusicQueue ;load win level music in event music queue -; inc FlagpoleMusicFlag ;set flag to keep music from getting queued more than once -; .else - lda ScrollLock ;if scroll lock not set, branch ahead to next part - beq ChkStop ;because we only need to do this part once - ; lda #EndOfLevelMusic - ; sta EventMusicQueue ;load win level music in event music queue - lda #$00 - sta ScrollLock ;turn off scroll lock to skip this part later -; .endif -ChkStop: - ; lda Player_CollisionBits ;get player collision bits - ; lsr ;check for d0 set - ; bcs RdyNextA ;if d0 set, skip to next part -; lda StarFlagTaskControl ;if star flag task control already set, -; bne InCastle ;go ahead with the rest of the code -; inc StarFlagTaskControl ;otherwise set task control now (this gets ball rolling!) -; InCastle: -; lda #%00100000 ;set player's background priority bit to -; sta Player_SprAttrib ;give illusion of being inside the castle -; RdyNextA: -; lda StarFlagTaskControl -; cmp #$05 ;if star flag task control not yet set -; bne ExitNA ;beyond last valid task number, branch to leave -; PrcNextA: -; inc LevelNumber ;increment level number used for game logic -; lda LevelNumber -; cmp #$03 ;check to see if we have yet reached level -4 -; bne NextArea ;and skip this last part here if not -; ldy WorldNumber ;get world number as offset -; lda CoinTallyFor1Ups ;check third area coin tally for bonus 1-ups -; cmp Hidden1UpCoinAmts,y ;against minimum value, if player has not collected -; bcc NextArea ;at least this number of coins, leave flag clear -; inc Hidden1UpFlag ;otherwise set hidden 1-up box control flag -; NextArea: - - ; jroweboy added: - ; disable the screen since loading a new area pointer will change CHR banks - ; inc DisableScreenFlag - - ; inc AreaNumber ;increment area number used for address loader - ; jsr LoadAreaPointer ;get new level pointer - ; inc FetchNewGameTimerFlag ;set flag to load new game timer - ; jsr ChgAreaMode ;do sub to set secondary mode, disable screen and sprite 0 - ; sta HalfwayPage ;reset halfway page to 0 (beginning) - ; lda #Silence - ; sta EventMusicQueue ;silence music and leave -ExitNA: - rts - ;------------------------------------------------------------------------------------- .reloc PlayerMovementSubs: diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 7496fd097..3595493a8 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -207,7 +207,7 @@ RESV TailWagCount ObjectOffset = $10 RESV FBall_OffscreenBits RESV AltEntranceControl -RESV PlayerEntranceCtrl +; RESV PlayerEntranceCtrl RESV JoypadOverride RESV Vine_Height RESV Player_State diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 82be43960..50ff6d46e 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2417,8 +2417,13 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra { rom.Put(ROM.ChrRomOffset + 0x1a0E0, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.dash.chr")); } - rom.UpdateSprite(props.CharSprite, true, props.ChangeItemSprites); - rom.UpdateSpritePalette(props.TunicColor, props.SkinTone, props.OutlineColor, props.ShieldColor, props.BeamSprite); + if (!props.MarioMode) + { + // Mario mode sprites and colors are too weird to allow changing it atm + // maybe after we fix the bugs with it... + rom.UpdateSprite(props.CharSprite, true, props.ChangeItemSprites); + rom.UpdateSpritePalette(props.TunicColor, props.SkinTone, props.OutlineColor, props.ShieldColor, props.BeamSprite); + } // Z2Mario, when making the vanilla romhack, change this flag to false const bool isRandomized = false; if (isRandomized) From e7b7500e60d72cfcedcc231b629fbba03271f2ce Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 23 May 2026 01:57:24 -0400 Subject: [PATCH 045/128] Fixup hammer recoil and remove lots of unused variables --- RandomizerCore/Asm/z2mario/integration.s | 37 ++++++++++++++++----- RandomizerCore/Asm/z2mario/mario.s | 20 ++++++------ RandomizerCore/Asm/z2r.inc | 41 +++++++++++++----------- 3 files changed, 62 insertions(+), 36 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index d9e331347..bfe77c607 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -621,8 +621,8 @@ SetupMarioControl: sta $69DE ; This is the color index 3 for the firey palette lda #8 sta GameEngineSubroutine - lda #1 ; Ground area type - sta AreaType + ; lda #1 ; Ground area type + ; sta AreaType lda #$28 ;store value here sta VerticalForceDown ;for fractional movement downwards if necessary jmp UpdatePlayerPalette @@ -956,7 +956,12 @@ PatchFireballHitcheck: ldy $11 lda Fireball_State,y and #%01000000 - bne @NoCollision ; hammer: pass through + beq @WasFireball ; bit 6 clear = fireball + ; Hammer landed, so mark this was a hammer so we prevent recoil later + lda #1 + sta HammerHitFlag,x + jmp @NoCollision ; hammer passes through: don't despawn it +@WasFireball: lda #%10000000 sta Fireball_State,y @NoCollision: @@ -1267,16 +1272,32 @@ SkipRecoilForStabs: .org $E66A rts +; Clear out the hammerhitflag when the enemy damage timer ends +.org $EF16 + jsr DecHitStateAndExpireHammerFlag +.reloc +DecHitStateAndExpireHammerFlag: + dec $040E,x + bne + + lda #0 + sta HammerHitFlag,x ; flash just ended + + + rts + .org $e395 jsr SkipEnemyRecoilIfThrowingHammer .reloc SkipEnemyRecoilIfThrowingHammer: - ; if we hit an enemy through a hammer, then we don't want to set the recoil + ; Suppress recoil for any enemy that was hit by a hammer, we set the flag earlier + ; if it was a hammer that caused this damage ldy ProjectileProcessing - beq + - lda #0 - + - sta $043e,x ; remove the recoil if it was hit by a hammer + bne @kill + ldy HammerHitFlag,x + beq @keep +@kill: + lda #0 +@keep: + sta $043e,x rts ; Make mario bounce off armored enemies diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index ec340eb9f..a42d06530 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -151,8 +151,8 @@ PlayerCtrlRoutine: ; prevent moving on death cmp #$0b beq SizeChk - lda AreaType ;are we in a water type area? - bne SaveJoyp ;if not, branch + ; lda AreaType ;are we in a water type area? + ; bne SaveJoyp ;if not, branch ldy Player_Y_HighPos dey ;if not in vertical area between bne DisJoyp ;status bar and bottom, branch @@ -930,8 +930,8 @@ X_Physics: ldy #$00 bcs GetXPhy ;if =>$19 branch here bcc ChkRFast ;if not branch elsewhere ProcPRun: iny ;if mario on the ground, increment Y - lda AreaType ;check area type - beq ChkRFast ;if water type, branch + ; lda AreaType ;check area type + ; beq ChkRFast ;if water type, branch dey ;decrement Y by default for non-water type area lda Left_Right_Buttons ;get left/right controller bits cmp Player_MovingDir ;check against moving direction @@ -1709,8 +1709,8 @@ PlayerBGUpperExtent: .byte $20, $10 PlayerBGCollision: - lda DisableCollisionDet ;if collision detection disabled flag set, - bne ExPBGCol ;branch to leave + ; lda DisableCollisionDet ;if collision detection disabled flag set, + ; bne ExPBGCol ;branch to leave ; clear the player metatile we are standing on sta PlayerStandingMetatile lda GameEngineSubroutine @@ -1785,10 +1785,10 @@ HeadChk: ; we can't check solid type here since its banked out ; jsr CheckForSolidMTiles ;check to see what player's head bumped on bcs SolidOrClimb ;if player collided with solid metatile, branch - ldy AreaType ;otherwise check area type - beq NYSpd ;if water level, branch ahead - ldy BlockBounceTimer ;if block bounce timer not expired, - bne NYSpd ;branch ahead, do not process collision + ; ldy AreaType ;otherwise check area type + ; beq NYSpd ;if water level, branch ahead + ; ldy BlockBounceTimer ;if block bounce timer not expired, + ; bne NYSpd ;branch ahead, do not process collision jsr PlayerHeadCollision ;otherwise do a sub to process collision jmp DoFootCheck ;jump ahead to skip these other parts diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 3595493a8..fef51fac4 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -200,20 +200,16 @@ RESV mapMarioPrevDirection ;;; ; MARIO related values that are not directly mapped to vanilla z2 object values -RESV JustThrownHammer -RESV TailSwingTimer -; SMB3 raccoon flutter: nonzero = Y-velocity cap active -RESV TailWagCount ObjectOffset = $10 -RESV FBall_OffscreenBits -RESV AltEntranceControl +; RESV FBall_OffscreenBits +; RESV AltEntranceControl ; RESV PlayerEntranceCtrl -RESV JoypadOverride -RESV Vine_Height +; RESV JoypadOverride +; RESV Vine_Height RESV Player_State -RESV DisableCollisionDet +; RESV DisableCollisionDet RESV GameEngineSubroutine -RESV AreaType +; RESV AreaType ; RESV A_B_Buttons A_B_Buttons = $741 RESV PreviousA_B_Buttons @@ -222,12 +218,12 @@ Left_Right_Buttons = $742 ; RESV Up_Down_Buttons Up_Down_Buttons = $743 RESV PlayerSize -RESV DeathMusicLoaded +; RESV DeathMusicLoaded RESV EventMusicQueue RESV EventMusicBuffer ; TODO use the vanilla music queues -RESV WarpZoneControl +; RESV WarpZoneControl RESV Player_CollisionBits ; Split Z2 and SMB1 collision bits -RESV HalfwayPage +; RESV HalfwayPage ;RESV SwimmingFlag SwimmingFlag = $13 RESV CurrentOAMOffset @@ -266,11 +262,10 @@ RESV ClimbSideTimer RESV JumpSwimTimer RESV RunningTimer RESV SideCollisionTimer -RESV BlockBounceTimer -RESV ChangeAreaTimer -FRAME_TIMER_COUNT = ChangeAreaTimer - Timers -RESV StarInvincibleTimer -ALL_TIMER_COUNT = StarInvincibleTimer - Timers +; RESV BlockBounceTimer +FRAME_TIMER_COUNT = SideCollisionTimer - Timers +; RESV StarInvincibleTimer +ALL_TIMER_COUNT = SideCollisionTimer - Timers ; StarInvincibleTimer - Timers RESV SpriteShuffleOffset ;RESV CurrentCHRBank @@ -297,6 +292,16 @@ RESV TailWag_CtlHold RESV TailWag_FreqLo RESV TailWag_FreqHi +RESV JustThrownHammer +RESV TailSwingTimer +; SMB3 raccoon flutter: nonzero = Y-velocity cap active +RESV TailWagCount +; Per-enemy "hit by hammer" flag. Persistent across the enemy's hit-flash so +; later per-enemy recoil setters ($9AF9 etc., which run after ProjectileProcessing +; is cleared) can still know to suppress the knockback. Cleared when the flash +; timer at $040E,x decrements to zero (see bank7_Display patch). +RESV HammerHitFlag, 6 + ; Shadow Mario Boss RAM CUSTOM_BOSS_START = RES_OFFSET RESV boss_animation From e2dfc641f33ebff882d9f8c192efbbb41ce5991e Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 23 May 2026 02:23:35 -0400 Subject: [PATCH 046/128] Remove more dead code, fix elevator areatype issue, mostly fix hammer offscreen --- RandomizerCore/Asm/z2mario/integration.s | 18 +++++++++++++----- RandomizerCore/Asm/z2mario/mario.s | 18 +++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index bfe77c607..a9aa35617 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -920,12 +920,20 @@ CheckHammerHitboxes: lda Fireball_State,y and #$40 beq + - jsr LoadProjectileCollisionBox - lda R0 + ; Inlined LoadProjectileCollisionBox ($E4BC) but with an offscreen guard + lda Fireball_X_Position,y + sec + sbc ScreenLeft_X_Pos sta HitboxXCoord - lda R1 - sta HitboxYCoord - jmp BreakBlockCollisionCheck + lda Fireball_PageLoc,y + sbc ScreenLeft_PageLoc + bne + + ; Hammer is onscreen so load setup its Y coord for the attack too + lda Fireball_Y_Position,y + clc + adc #$04 + sta HitboxYCoord + jmp BreakBlockCollisionCheck + rts diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index a42d06530..b4f6fca5b 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -153,15 +153,15 @@ PlayerCtrlRoutine: beq SizeChk ; lda AreaType ;are we in a water type area? ; bne SaveJoyp ;if not, branch - ldy Player_Y_HighPos - dey ;if not in vertical area between - bne DisJoyp ;status bar and bottom, branch - lda Player_Y_Position - cmp #$d0 ;if nearing the bottom of the screen or - bcc SaveJoyp ;not in the vertical area between status bar or bottom, -DisJoyp: - lda #$00 ;disable controller bits - sta SavedJoypadBits + ; ldy Player_Y_HighPos +; dey ;if not in vertical area between +; bne DisJoyp ;status bar and bottom, branch +; lda Player_Y_Position +; cmp #$d0 ;if nearing the bottom of the screen or +; bcc SaveJoyp ;not in the vertical area between status bar or bottom, +; DisJoyp: +; lda #$00 ;disable controller bits +; sta SavedJoypadBits SaveJoyp: lda SavedJoypadBits ;otherwise store A and B buttons in $0a and #%11000000 From d84b8fa38d4b006e4d1d59dcc8af0afbe356ebfa Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 23 May 2026 23:37:59 -0400 Subject: [PATCH 047/128] Add HOT NEW L O R E for z2mario --- RandomizerCore/ROM.cs | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index bc673f3cf..8c0a203fe 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -607,7 +607,38 @@ .byte TitleEnd - Line7 .byte $20,$1F,VERTICAL | REPEAT | $1f,$FD .assert * = $b02e -""", "randomizer_title_text.s"); + +""", "randomizer_title_gamename.s"); + + // Also update the text on the title screen to our hot new L O R E + // There's 15 lines with 29 columns of tiles, with $fd as the 29th tile + // Realistically we can use 26 or so letters a line for it to be readable + // cause of the wall of sprites on the right + var titleText = """ +IN A LOCAL MILK BAR, LINK +AND MARIO SWAPPED TALES OF +PRINCESS PROBLEMS. MARIO +SCOFFED WHEN LINK IMPLIED +THAT SAVING ZELDA WAS SO +MUCH HARDER, AFTER ALL, +ENEMIES IN HYRULE ARE WAY +SMARTER THAN A SILLY KOOPA. +MARIO, IN A MILK-FILLED +HAZE, STUMBLED DOWN THE +WRONG PIPE AND WOKE UP IN +HYRULE. CAN HE ACTUALLY +SAVE PRINCESS ZELDA... +""".Trim(); + var a = asm.Module(); + a.Segment("PRG5"); + foreach (var (line, i) in titleText.Split('\n').Select((item, index) => (item, index))) + { + var blankLine = Enumerable.Repeat((byte)0xf4,29).ToArray(); + StringToZ2Bytes(line.Trim()).CopyTo(blankLine, 0); + blankLine[28] = 0xfd; + a.Org((ushort) (0xA932 + i * 29)); + a.Byt(blankLine); + } } public void ApplyIps(byte[] patch, bool expandRom = false) @@ -2341,6 +2372,9 @@ public static byte[] StringToZ2Bytes(string text) { ' ', 0xf4 }, { '-', 0xf6 }, { '\n', 0xfd }, + // Extended characters on the title screen + { ',', 0x9c }, + { '©', 0x0e }, }; private static readonly IDictionary ReverseCharMap = CharMap.ToDictionary(x => x.Value, x => x.Key); From 732adefde4e6fb1d532e8a504924db875b58a723 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 25 May 2026 17:27:02 -0400 Subject: [PATCH 048/128] Clear out shield on death and lives screen --- CommandLine/Properties/launchSettings.json | 2 +- RandomizerCore/Asm/z2mario/integration.s | 7 +++++++ RandomizerCore/ROM.cs | 12 ++++++------ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CommandLine/Properties/launchSettings.json b/CommandLine/Properties/launchSettings.json index 2593b1149..38f4ce70b 100644 --- a/CommandLine/Properties/launchSettings.json +++ b/CommandLine/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "CommandLine": { "commandName": "Project", - "commandLineArgs": "-f AAABvvyhAmLAAAAAAAAAEuPk+hAAAAAAAAAAAAAmA -r ../Zelda2.nes -s 32165874 -o ..", + "commandLineArgs": "-f AAABvvyhAmLAAAAAAAAAEuPk+hAAAAAAAAAAAAAsA -r ../Zelda2.nes -s 32165874 -o ..", "workingDirectory": "../build/" } } diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index a9aa35617..b07ec7c3b 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -98,6 +98,10 @@ SetHurtMetasprite: .import METASPRITE_SMALL_MARIO_DEATH lda #METASPRITE_SMALL_MARIO_DEATH sta ObjectMetasprite + ; Clear all spells while here too + lda #0 + sta $076f + sta $070f lda #$0b ; player death subroutine sta GameEngineSubroutine rts @@ -113,6 +117,9 @@ PatchLinkDeathSprite: sta ObjectMetasprite lda #0 sta boss_animation + ; Clear all spells. Fixes oddities when dying with something cast + sta $076f + sta $070f lda #$0b ; player death subroutine sta GameEngineSubroutine jsr PlayerGfxHandler diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 8c0a203fe..595fbee73 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -615,18 +615,18 @@ .byte TitleEnd - Line7 // Realistically we can use 26 or so letters a line for it to be readable // cause of the wall of sprites on the right var titleText = """ -IN A LOCAL MILK BAR, LINK -AND MARIO SWAPPED TALES OF +IN A LOCAL MILK BAR, MARIO +AND LINK TRADED TALES OF PRINCESS PROBLEMS. MARIO SCOFFED WHEN LINK IMPLIED THAT SAVING ZELDA WAS SO MUCH HARDER, AFTER ALL, ENEMIES IN HYRULE ARE WAY SMARTER THAN A SILLY KOOPA. -MARIO, IN A MILK-FILLED -HAZE, STUMBLED DOWN THE -WRONG PIPE AND WOKE UP IN -HYRULE. CAN HE ACTUALLY +BLINDED BY MILK-FUELED RAGE, +MARIO STOMPED DOWN THE PIPE +TO HYRULE TO PROVE LINK +WRONG. BUT CAN HE ACTUALLY SAVE PRINCESS ZELDA... """.Trim(); var a = asm.Module(); From d02077092944529f6f2944ec35042ebf67cbf63b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 26 May 2026 02:07:03 -0400 Subject: [PATCH 049/128] Remove unused smb1 noise sfx and try to make indoor theme play --- RandomizerCore/Asm/z2mario/integration.s | 35 ++++++++++++------------ RandomizerCore/Music.cs | 10 ++++++- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index b07ec7c3b..193b8e32b 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1226,25 +1226,26 @@ CheckMarioSq2Sfx: jmp $9408 ; Original square 2 sfx from z2 .reloc CheckMarioNoiseSfx: - lda $07E1 ; L990B active effect type (non-zero = L990B is driving Noise) - bne @Z2ExtraActive - jsr NoiseSfxHandler - lda NoiseSoundBuffer - beq @PlayingZ2 - lda #$ff - sta Z2NoiseSoundBuffer - rts -@PlayingZ2: - lda #$ff - cmp Z2NoiseSoundBuffer - bne @ActualNoiseSfx + ; We don't use any mario noise sfx, so we can skip processing it. + ; lda $07E1 ; L990B active effect type (non-zero = L990B is driving Noise) + ; bne @Z2ExtraActive + ; jsr NoiseSfxHandler + ; lda NoiseSoundBuffer + ; beq @PlayingZ2 + ; lda #$ff + ; sta Z2NoiseSoundBuffer + ; rts +; @PlayingZ2: + ; lda #$ff + ; cmp Z2NoiseSoundBuffer + ; bne @ActualNoiseSfx ; this is the fake sfx id we used to signal that this was a mario buffer ; but now we can clear it. - lda #0 - sta Z2NoiseSoundBuffer -@Z2ExtraActive: - rts -@ActualNoiseSfx: + ; lda #0 + ; sta Z2NoiseSoundBuffer +; @Z2ExtraActive: + ; rts +; @ActualNoiseSfx: jmp $95A7 ; Original "complex" sfx from z2 .segment "PRG7" diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index e43315c28..4402009fb 100644 --- a/RandomizerCore/Music.cs +++ b/RandomizerCore/Music.cs @@ -28,6 +28,8 @@ internal enum Usage Boss, LastBoss, Credits, + GameComplete, + House, } /*internal class LoggerAdapter : Logger @@ -97,9 +99,11 @@ record BuiltinSongInfo(int Index, string Title, Usage[] Uses); new(1, "Overworld", [Usage.Overworld]), new(3, "Battle", [Usage.Encounter, Usage.Cave]), new(5, "Town", [Usage.Town]), + new(7, "House", [Usage.House]), new(9, "Palace", [Usage.Palace]), new(0xb, "Boss", [Usage.Boss]), new(0xd, "Great Palace", [Usage.GreatPalace]), + new(0xf, "GameComplete", [Usage.GameComplete]), new(0x10, "Credits", [Usage.Credits]), new(0x12, "Last Boss", [Usage.LastBoss]), }; @@ -201,6 +205,8 @@ public void ImportSongs() { Usage.Boss, 1 }, { Usage.LastBoss, 1 }, { Usage.Credits, 1 }, + { Usage.GameComplete, 1 }, + { Usage.House, 4 }, }; var selUsesSongs = _imptr.SelectUsesSongs( usesSongs, numUsageSongs, _shuffler); @@ -312,6 +318,7 @@ SongMap CreateSongMap(UsesSongs usesSongs) { Usage.Boss, 0xb }, { Usage.LastBoss, 0x12 }, { Usage.Credits, 0x10 }, + { Usage.GameComplete, 0xf }, }; SongMap songMap = new(); @@ -339,6 +346,7 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) Usage.GreatPalace, Usage.Cave, Usage.Encounter, + Usage.House }.ToDictionary(k => k, k => new List()); foreach (var world in _hyrule.worlds) @@ -354,7 +362,7 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) /*|| loc.ActualTown == Town.SARIA_SOUTH*/) continue; - usage = Usage.Town; + usage = (!loc.AppearsOnMap) ? Usage.House : Usage.Town; } else if (loc.TerrainType == Terrain.PALACE) { From 0a2f28e1d71b9ea7427355c9b07a7749a9bf13aa Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 26 May 2026 08:35:46 -0400 Subject: [PATCH 050/128] Add 1up and lives sprite --- RandomizerCore/Asm/z2mario/tanooki_tail.chr | Bin 160 -> 256 bytes RandomizerCore/Hyrule.cs | 11 ++++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/tanooki_tail.chr b/RandomizerCore/Asm/z2mario/tanooki_tail.chr index c3b03d1fe2b8d801cdcbbb6410f804c932d2f6df..24ccdfe0dd008d9c00056d47aca07439f5954466 100644 GIT binary patch delta 103 zcmZ3$*uXSlK>`CBVCEMVw6L|4VdjyMQ&%>UVUW>EwhMKXVPFuEsjsiMORlfivNJO) gkBxNnrRFS0I~WKbpQYW delta 6 NcmZo*TEIAA0RRV60#pD1 diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 50ff6d46e..310cda119 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2438,11 +2438,16 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra { // Overwrite link's downstab animation with fireball explosion sprites var fireballExplosion = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.fireball_explosion.chr"); - var tanooki_tail = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.tanooki_tail.chr"); + var extra_sprites = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.tanooki_tail.chr"); + var tanooki_tail = extra_sprites[..0xc0]; + var oneup = extra_sprites[0xc0..0xe0]; + var lifecount = extra_sprites[0xe0..0xf0]; for (var i = 0; i < 13; i++) { - rom.Put(ROM.ChrRomOffset + 0x400 + (i * 0x2000), fireballExplosion); - rom.Put(ROM.ChrRomOffset + 0x560 + (i * 0x2000), tanooki_tail); + rom.Put(ROM.ChrRomOffset + 0x0400 + (i * 0x2000), fireballExplosion); + rom.Put(ROM.ChrRomOffset + 0x0560 + (i * 0x2000), tanooki_tail); + rom.Put(ROM.ChrRomOffset + 0x0a80 + (i * 0x2000), oneup); + rom.Put(ROM.ChrRomOffset + 0x1960 + (i * 0x2000), lifecount); } rom.Put(ROM.ChrRomOffset + 0x1a800, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.sprites_mario_hammer.chr")); rom.Put(ROM.ChrRomOffset + 0x1b800, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.sprites_mario_tanooki.chr")); From 12b5f5c9340c3ddee6a2df590eb92ab46f0a1ae9 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 26 May 2026 08:55:17 -0400 Subject: [PATCH 051/128] Put tail sprite behind mud --- RandomizerCore/Asm/z2mario/integration.s | 6 ++++++ RandomizerCore/Asm/z2mario/metasprite.s | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 193b8e32b..a0b8ce0ea 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -485,6 +485,12 @@ Yhi = R7 @flipRight: lda #0 sta Atr + + ; if stuck in mud/swamp copy over the render behind flag + lda $0752 + and #$20 + ora Atr + sta Atr jmp MetaspriteRenderLoop diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index eeaeecf83..2fe75d7bd 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -462,10 +462,12 @@ SetYOffset: rts @isplayer: ; We are rendering a mario sprite, so check if we are stuck in the mud + ; This is a bit hacky since we want to only apply it to the bottom half + ; of big mario, or all of small mario lda $0752 and #$20 beq @exit - ; stuck in the mud, so update bottom two sprite + ; stuck in the mud/swamp, so update bottom two sprite ; x = next oam sprite id, so we can offset by 8 to hit bottom two sprites ldx CurrentOAMOffset ; if we are small then its the most recent two sprites. if we are big then its 4 sprites back From 3b833f8183559b20d86a997792d080381fee936c Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 26 May 2026 13:15:17 -0400 Subject: [PATCH 052/128] Don't clamp mario position when scroll locked --- RandomizerCore/Asm/z2mario/integration.s | 4 ++++ RandomizerCore/Asm/z2mario/mario.s | 3 +++ 2 files changed, 7 insertions(+) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index a0b8ce0ea..ead599c08 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -814,6 +814,10 @@ NoDecTimers: ; Check scroll lock. If we are locked in, then prevent leaving the screen lda ScrollLock beq @SkipScrollLock + ; don't apply scroll lock when we aren't in player control + lda GameEngineSubroutine + cmp #8 + bne @SkipScrollLock lda Player_X_Position cmp #3 bcc @LockLeft diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index b4f6fca5b..0918f5faa 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -527,6 +527,9 @@ FinishedInjuryBlink: ; Z2Mario - clear scroll lock after you change size ; lda #0 dec ScrollLock + ; and also reset the injurytimer to extend your invincibility + lda #$20 + sta InjuryTimer jmp DonePlayerTask ;------------------------------------------------------------------------------------- ; .reloc From e046994515b7ead0ae64c988e57dece0c19f288a Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 27 May 2026 17:58:54 -0400 Subject: [PATCH 053/128] Fix fairy glitching through top of elevator --- RandomizerCore/Asm/z2mario/integration.s | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index ead599c08..6ff38e6a8 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -192,6 +192,11 @@ BankPatchLinkDrawRoutine: pla rts +; Update z2 block collision check to use the regular link hitbox for fairy +; by just skipping the y+1 it does to set the fairy size offset +.org $e07f + jmp *+3 + .org $E1A2 jsr CheckIfAboveScreenForFallingFairyCheck nop From 4d1ad5371c9a79a8289454e4a32d21d0dc067478 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 27 May 2026 21:04:54 -0400 Subject: [PATCH 054/128] Remove unused tail sprite, remove house music, try to fix final cutscene with tanooki --- RandomizerCore/Asm/z2mario/boss.s | 4 ++++ RandomizerCore/Hyrule.cs | 2 +- RandomizerCore/Music.cs | 3 +-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index 74ccef33d..f9cd33f9a 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -1051,6 +1051,10 @@ KillBoss: sta $0751 lda #$FF sta $504 ; stop link from moving + ; Clear out links magic to fix weird issues + lda #0 + sta $076f + inc ReloadCHRBank @animating: jsr MoveD_EnemyVertically ; gravity decelerates upward motion then pulls boss down dec boss_counter diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 310cda119..d05ab608c 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2439,7 +2439,7 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra // Overwrite link's downstab animation with fireball explosion sprites var fireballExplosion = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.fireball_explosion.chr"); var extra_sprites = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.tanooki_tail.chr"); - var tanooki_tail = extra_sprites[..0xc0]; + var tanooki_tail = extra_sprites[..0xa0]; // theres an extra unused tail tile in here var oneup = extra_sprites[0xc0..0xe0]; var lifecount = extra_sprites[0xe0..0xf0]; for (var i = 0; i < 13; i++) diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index 4402009fb..1e2a215e1 100644 --- a/RandomizerCore/Music.cs +++ b/RandomizerCore/Music.cs @@ -346,7 +346,6 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) Usage.GreatPalace, Usage.Cave, Usage.Encounter, - Usage.House }.ToDictionary(k => k, k => new List()); foreach (var world in _hyrule.worlds) @@ -362,7 +361,7 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) /*|| loc.ActualTown == Town.SARIA_SOUTH*/) continue; - usage = (!loc.AppearsOnMap) ? Usage.House : Usage.Town; + usage = Usage.Town; } else if (loc.TerrainType == Terrain.PALACE) { From 22197d160900833fa69e38ea6ae62cbbe8b61557 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 27 May 2026 21:23:46 -0400 Subject: [PATCH 055/128] Fix boss fireball spawn position and add UNLIKE LINK instead of the end --- RandomizerCore/Asm/z2mario/boss.s | 6 ++++++ RandomizerCore/Asm/z2mario/integration.s | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index f9cd33f9a..9dd0c02fb 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -775,7 +775,12 @@ BossSpawnFireball: clc adc #4 sta SprObject_Y_Position,y + lda Enemy_MovingDir,x + lsr lda Enemy_X_Position,x + bcc + + adc #15 + + sta SprObject_X_Position,y lda Enemy_PageLoc,x sta SprObject_PageLoc,y @@ -1054,6 +1059,7 @@ KillBoss: ; Clear out links magic to fix weird issues lda #0 sta $076f + sta $d0 inc ReloadCHRBank @animating: jsr MoveD_EnemyVertically ; gravity decelerates upward motion then pulls boss down diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 6ff38e6a8..5658100a4 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1497,4 +1497,11 @@ CheckIfPlayerSmall: .byte $10 ; small Y offset .byte $00 ; big Y offset .byte $0f ; small Y length - .byte $1d ; big Y length \ No newline at end of file + .byte $1d ; big Y length + +; Replace the THE END text with UNLIKE LINK +.segment "PRG5" +.org $8F8F + ; U N L I K E L I N K +.byte $23,$14,$0b,$ee,$e7,$e5,$e2,$e4,$de,$f4,$e5,$e2,$e7,$e4,$ff +.assert * = $8f9e \ No newline at end of file From 7a3cd80801a2684f4e5de6cd105f860ee1219048 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 28 May 2026 00:20:57 -0400 Subject: [PATCH 056/128] Add TANOOKI spell name --- RandomizerCore/CustomTexts.cs | 12 ++++++++---- RandomizerCore/Hyrule.cs | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 0e09c2b12..b55c9f256 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -528,7 +528,7 @@ public static List GenerateTexts( texts[70] = new Text("USE THIS$TO GO$FAST"); } - GenerateWizardTexts(texts, locations, nonhashRNG, props.UseCommunityText); + GenerateWizardTexts(texts, locations, nonhashRNG, props.UseCommunityText, props.MarioMode); if (props.SpellItemHints) { @@ -699,7 +699,7 @@ private static Text GenerateTownSignHint(Collectable spell, bool linkedFire) return new Text(text); } - private static Text GenerateWizardText(List texts, Random r, Location location, bool useCommunityText) + private static Text GenerateWizardText(List texts, Random r, Location location, bool useCommunityText, bool z2MarioMode) { if(location.ActualTown == null) { @@ -719,6 +719,10 @@ private static Text GenerateWizardText(List texts, Random r, Location loca .Union(WIZARD_SPELL_TEXTS_BY_COLLECTABLE[collectable]).ToList(); int selectedHintIndex = r.Next(possibleWizardHints.Count()); return new Text(possibleWizardHints[selectedHintIndex]); + } else if (z2MarioMode && collectable == Collectable.JUMP_SPELL) + { + // In Mario Mode, change the JUMP spell hint text to reflect its new ability + return new Text("JUMP WONT$HELP YOU.$TRY THIS$INSTEAD."); } //Non community-text spells use the vanilla text corresponding to the spell you get. return new Text(texts[wizardTextIndexesBySpell[collectable]].RawText); @@ -1023,7 +1027,7 @@ private static void GenerateSpellHints(IEnumerable locations, List texts, IEnumerable itemLocs, Random r, bool useCommunityText) + private static void GenerateWizardTexts(List texts, IEnumerable itemLocs, Random r, bool useCommunityText, bool z2MarioMode) { List vanillaText = new(texts); List usedWizardTexts = []; @@ -1038,7 +1042,7 @@ private static void GenerateWizardTexts(List texts, IEnumerable int tries = 0; do { - wizardHint = GenerateWizardText(vanillaText, r, location, useCommunityText); + wizardHint = GenerateWizardText(vanillaText, r, location, useCommunityText, z2MarioMode); } while (useCommunityText && usedWizardTexts.Contains(wizardHint) && tries++ < 100); usedWizardTexts.Add(wizardHint); texts[townWizardTextIndexes[(Town)location.ActualTown]] = wizardHint; diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index d05ab608c..dbd25c9af 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3840,7 +3840,11 @@ private void MarioModeActivate(Assembler asm) a.Assign("ENABLE_Z2_MARIO", 1); a.Code(Util.ReadResource($"Z2Randomizer.RandomizerCore.Asm.z2mario.{mod}"), mod); } - + // Rename JUMP to TANOOKI + var m = asm.Module(); + m.Segment("PRG0"); + m.Org(0x9c38); + m.Byt(Util.ToGameText("TANOOKI")); } private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Random RNG, List texts, ROM rom, StatRandomizer randomizedStats) From 2659486ac576de65cf59ef5beb2e75786998aede Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 28 May 2026 00:21:22 -0400 Subject: [PATCH 057/128] Fix elimination mode playing music in title screen instead of delete sfx --- RandomizerCore/Asm/MMC5.s | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RandomizerCore/Asm/MMC5.s b/RandomizerCore/Asm/MMC5.s index 60b14374a..df5e89265 100644 --- a/RandomizerCore/Asm/MMC5.s +++ b/RandomizerCore/Asm/MMC5.s @@ -133,6 +133,7 @@ LagFrameVar = $100 .else UpdateSound = $9000 .endif +UpdateTitleScreenSound = $800a .segment "PRG6" @@ -205,8 +206,12 @@ HandleLagFrame: beq @RunAudio ; Audio is currently running on the main thread; signal it to re-run after dec PendingAudioCall +@TitleScreenSoSkip: rts @RunAudio: + ; Run title screen music if we are in the title screen instead + bit LagFrameVar + bvc @TitleScreenSoSkip jmp UpdateSound .segment "PRG7" From c298b2add1dcc6864a206b5700a0e181608023fe Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 28 May 2026 00:21:59 -0400 Subject: [PATCH 058/128] Fix hammer giving mario speed and add darunia chimney support --- RandomizerCore/Asm/z2mario/integration.s | 8 +++++++ RandomizerCore/Asm/z2mario/mario.s | 29 +++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 5658100a4..e15a689fd 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -192,6 +192,14 @@ BankPatchLinkDrawRoutine: pla rts +.org $e0e2 + jsr PatchGoingDownChimney +.reloc +PatchGoingDownChimney: + inc $070e ; link go down de hole flag/timer + lda #Sfx_PipeDown_Injury + sta Square1SoundQueue ; play pipe sound + rts ; Update z2 block collision check to use the regular link hitbox for fairy ; by just skipping the y+1 it does to set the fairy size offset .org $e07f diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 0918f5faa..88588718c 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -44,6 +44,29 @@ ; sta $0774 ;infinite health ;.endif +; check for going down the chimney in darunia + lda $070E ; chimney sinking state (0 == not sinking non zero == sink away) + beq @continueMario + inc $070E ; acts as a timer for sinking now + lda $070E + cmp #$38 + bcs @finishedSinking + inc Player_Y_Position + lda #$20 + sta $0752 + sta SprObject_SprAttrib + ldx #METASPRITE_BIG_MARIO_CROUCHING + ldy #0 + .import DrawMetasprite + jmp DrawMetasprite +@finishedSinking: + inc $075B + lda #0 + sta $0752 + sta SprObject_SprAttrib + lda #$16 + jmp $E187 +@continueMario: lda GameEngineSubroutine ;run routine based on number (a few of these routines are jsr JumpEngine ;merely placeholders as conditions for other routines) @@ -2217,7 +2240,7 @@ FireballObjCore: lda #4 ;set vertical speed of fireball sta Fireball_Y_Speed,x - lda #0 ; Don't add extra gravity ? + lda #0 ; Don't add extra gravity (probs not needed?) sta SprObject_X_MoveForce + FireballOffset,x sta SprObject_Y_MoveForce + FireballOffset,x ; lda #$07 @@ -2615,8 +2638,8 @@ HammerXPosition: asl asl asl - adc SprObject_X_MoveForce,x - sta SprObject_X_MoveForce,x + adc SprObject_X_MoveForce + FireballOffset,x + sta SprObject_X_MoveForce + FireballOffset,x php lda Fireball_X_Speed,x From 8e91f227f28f44b8eeecf1205e4979167b6b0ccb Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 28 May 2026 09:38:37 -0400 Subject: [PATCH 059/128] Free up space by putting ganon laugh in PRG1F --- RandomizerCore/Asm/Init.s | 16 ++++- RandomizerCore/Asm/MMC5.s | 105 +++++++++++++++++++++--------- RandomizerCore/Asm/StatTracking.s | 32 ++++++++- RandomizerCore/Hyrule.cs | 12 +--- RandomizerCore/ROM.cs | 40 +++++++++++- 5 files changed, 157 insertions(+), 48 deletions(-) diff --git a/RandomizerCore/Asm/Init.s b/RandomizerCore/Asm/Init.s index 360031d45..42699042d 100644 --- a/RandomizerCore/Asm/Init.s +++ b/RandomizerCore/Asm/Init.s @@ -78,10 +78,19 @@ FREE "PRG5" [$bda1, $c000) ;FREE "PRG6" [$ac09, $c000) TEMP FREE "PRG6" [$ac21, $c000) +; FREE "PRG7" [$d39a, $d3c2) ; DPCM data, will affect dpcm sfx but not gameplay so its fine to use this as a last ditch ; free space for patches. Keep it disabled as much as possible ; FREE "PRG7" [$f369, $fcfb) -FREE "PRG7" [$f3d0, $fcfb) ; allow code in the ganon laugh sfx but not the hurt sfx +; allow code in the ganon laugh sfx but not the hurt sfx +; This is fine because we now put the ganon laugh in PRG1F instead +FREE "PRG7" [$f3d0, $fcfb) +; carve around the ganon sfx and hope it still fits +; FREE "PRG7" [$f581, $f700) +; FREE "PRG7" [$f814, $f840) +; FREE "PRG7" [$f955, $f9c0) +; FREE "PRG7" [$fb71, $fcfb) +; FREE "PRG7" [$FEAA, $FED0) ; Currently these are only used as space for music tracks ;FREE "PRG10" [$a000, $c000) @@ -101,4 +110,7 @@ FREE "PRG1D" [$a000, $c000) FREE "PRG1E" [$a000, $c000) ; Most of bank 1f is reserved for z2ft -FREE "PRG1F" [$ff80, $ffe8) +; But we have some small gaps we can slide in between the DPCM samples +FREE "PRG1F" [$fad6, $fb00) +FREE "PRG1F" [$fc14, $fc40) +FREE "PRG1F" [$fe31, $ffe8) diff --git a/RandomizerCore/Asm/MMC5.s b/RandomizerCore/Asm/MMC5.s index df5e89265..bb4a6b05f 100644 --- a/RandomizerCore/Asm/MMC5.s +++ b/RandomizerCore/Asm/MMC5.s @@ -215,7 +215,6 @@ HandleLagFrame: jmp UpdateSound .segment "PRG7" - .reloc RunAudioFrameOrLagFrame: pha @@ -249,6 +248,8 @@ RunAudioFrameOrLagFrame: pla rti +.segment "PRG7" + ; Disable the final sta PPUCTRL in NMI if we handle that in the IRQ instead .org $C1B1 jmp *+3 @@ -275,7 +276,6 @@ Nmi: .assert * = $C085 ; Also run the stat timers during the title/menu in case they push the reset button -.pushseg .segment "PRG5" .org $A612 ; Change this from and #$7c to $fc to keep NMI running @@ -300,9 +300,8 @@ IncStatTimerTitle: sta SoftDisableNmi ora $0747 rts -.popseg - +.segment "PRG7" ; Add a patch to increment the stat timer every NMI ; This is cleared when a new save file is loaded for the first time .reloc @@ -341,6 +340,7 @@ IncStatTimer: asl adc PalaceRegionIndex tay +.import PalaceMappingTable ldx PalaceMappingTable,y @IncrementTimer: inc StatTimeAtLocation+0,x @@ -352,32 +352,7 @@ IncStatTimer: @Exit: rts -; These values are exported from the randomizer and map from internal palace number -; to the "real palace" at this offset. This way if Palace 5 is in the location of Palace 1, -; then we increment the correct timer for Palace 5 -.reloc -Palace1Offset = StatTimeInPalace1 - StatTimeAtLocation -.export PalaceMappingTable -PalaceMappingTable: - ; region 0 - east hyrule - .byte RealPalaceAtLocation1 * 3 + Palace1Offset - .byte RealPalaceAtLocation2 * 3 + Palace1Offset - .byte RealPalaceAtLocation3 * 3 + Palace1Offset - .byte $ff ; unused 4th palace in region 0 - ; region 1 - death mountain - .byte $ff ; unused 1st palace in region 1 - .byte $ff ; unused 2nd palace in region 1 - .byte $ff ; unused 3th palace in region 1 - .byte $ff ; unused 4th palace in region 1 - ; region 2 - west hyrule - .byte RealPalaceAtLocation5 * 3 + Palace1Offset - .byte RealPalaceAtLocation6 * 3 + Palace1Offset - .byte RealPalaceAtLocationGP * 3 + Palace1Offset - .byte $ff ; unused 4th palace in region 2 - ; region 3 - maze island - .byte RealPalaceAtLocation4 * 3 + Palace1Offset - .byte $ff, $ff, $ff ; 3 unused palace locations - +.segment "PRG7" ; Screen split IRQ implementation. This is not technically a part of z2ft, but due to the policy of not making the game faster than vanilla, this optimization is only enabled when z2ft is enabled to partially offset the cost of FT playback. ; Patch locations that hard disable NMI to set the soft disable instead @@ -475,7 +450,8 @@ bank1f_reset: sta PrgBankEReg jmp bank1f_reset_part2 -.org $fffc +.org $fffa + .word (Bank1f_Nmi) .word (bank1f_reset) .word (bank1f_reset) @@ -669,6 +645,73 @@ UPDATE_REFS SwapToSavedPRG @ $E002 .endif + +; Clear a ton of free space out of the fixed bank by banking ganon's laugh sfx +; its only used during a scene thats otherwise doing nothing special, so we can +; cheese it out using mmc5's ability to bank $e000. +.segment "PRG7" + +.org $CA72 + lda #$80 + sta SoftDisableNmi + lda #$ff ; place the final bank in + sta PrgBankEReg + jmp GanonLaughingTeeHee +SwapBackFromBank1f: + sta PrgBankEReg + jmp $CF05 +.assert * <= $CA85 + +.segment "PRG1F" +.reloc +Bank1f_Nmi: + inc StatTimer + bne @skip + inc StatTimer+1 + bne @skip + inc StatTimer+2 +@skip: + pha + lda $FE + sta PPUMASK + pla + rti +.reloc +GanonLaughingTeeHee: + lda #0 + sta $0726 + lda #(6*2) | PRG_BANK_ROM + sta PrgBank8Reg + lda #(6*2+1) | PRG_BANK_ROM + sta PrgBankAReg + ; re-enable rendering + lda $FE + ora #$18 + ora $0768 + sta $FE +@loop: + ; Wait for NMI each frame + lda StatTimer +@WaitForNMI: + cmp StatTimer + beq @WaitForNMI + jsr UpdateSound + jsr $D346 ; bank7_Controllers_Input ; this is in $d000 so its safe + lda $F7 + and #$10 + bne @donewaiting + lda $0501 + beq @donewaiting + dec $0501 + jmp @loop +@donewaiting: + ldx #1 + stx $07EE ; set to skip to the last sfx + dex + stx SoftDisableNmi + lda #((7*2 + 1) | PRG_BANK_ROM); place the final bank in + jmp SwapBackFromBank1f + .segment "PRG0" ; move the scrolling code that loads metatiles from the current area diff --git a/RandomizerCore/Asm/StatTracking.s b/RandomizerCore/Asm/StatTracking.s index 334385102..6f2bf7883 100644 --- a/RandomizerCore/Asm/StatTracking.s +++ b/RandomizerCore/Asm/StatTracking.s @@ -116,7 +116,35 @@ StatTrackDownStab: sta $80 rts .endif - + +.segment "PRG7" + +; These values are exported from the randomizer and map from internal palace number +; to the "real palace" at this offset. This way if Palace 5 is in the location of Palace 1, +; then we increment the correct timer for Palace 5 +.reloc +Palace1Offset = StatTimeInPalace1 - StatTimeAtLocation +.export PalaceMappingTable +PalaceMappingTable: + ; region 0 - east hyrule + .byte RealPalaceAtLocation1 * 3 + Palace1Offset + .byte RealPalaceAtLocation2 * 3 + Palace1Offset + .byte RealPalaceAtLocation3 * 3 + Palace1Offset + .byte $ff ; unused 4th palace in region 0 + ; region 1 - death mountain + .byte $ff ; unused 1st palace in region 1 + .byte $ff ; unused 2nd palace in region 1 + .byte $ff ; unused 3th palace in region 1 + .byte $ff ; unused 4th palace in region 1 + ; region 2 - west hyrule + .byte RealPalaceAtLocation5 * 3 + Palace1Offset + .byte RealPalaceAtLocation6 * 3 + Palace1Offset + .byte RealPalaceAtLocationGP * 3 + Palace1Offset + .byte $ff ; unused 4th palace in region 2 + ; region 3 - maze island + .byte RealPalaceAtLocation4 * 3 + Palace1Offset + .byte $ff, $ff, $ff ; 3 unused palace locations + .segment "PRG4" @@ -141,7 +169,7 @@ PalaceTable: .byte RealPalaceAtLocation2 + TsPalace1 .byte RealPalaceAtLocation3 + TsPalace1 .byte $ff ; unused 4th palace in region 0 - ; region 1 - Death Mountain + ; region 1 - Death Mountain .byte $ff ; unused 1st palace in region 1 .byte $ff ; unused 2nd palace in region 1 .byte $ff ; unused 3th palace in region 1 diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index dbd25c9af..9d1310def 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3819,16 +3819,6 @@ .word NewCreditsBody """, "add_credits.s"); } - private void ChangeMapperToMMC5(Assembler asm, bool preventFlash, bool enableZ2Ft, bool enableZ2Mario) - { - var a = asm.Module(); - a.Assign("PREVENT_HUD_FLASH_ON_LAG", preventFlash ? 1 : 0); - a.Assign("ENABLE_Z2FT", enableZ2Ft ? 1 : 0); - if (enableZ2Mario) - a.Assign("ENABLE_Z2_MARIO", 1); - AssignRealPalaceLocations(a); - a.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.MMC5.s"), "mmc5_conversion.s"); - } private void MarioModeActivate(Assembler asm) { @@ -3851,7 +3841,7 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando { bool randomizeMusic = !props.DisableMusic && props.RandomizeMusic; - ChangeMapperToMMC5(engine, props.DisableHUDLag, randomizeMusic, props.MarioMode); // will make output vary with customize tab options + rom.ChangeMapperToMMC5(engine, props.DisableHUDLag, randomizeMusic, props.MarioMode); // will make output vary with customize tab options // the second flag determines if we should add the "randomizer" text, but we don't have a "vanilla" option // right now, so this is just always off, except when i manually make an update rom.AddRandomizerToTitle(engine, props.MarioMode, false); diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 595fbee73..314bac6c9 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -434,6 +434,37 @@ public void WritePalacePalettes(List bricks, List curtains, List Date: Thu, 28 May 2026 11:48:33 -0400 Subject: [PATCH 060/128] Fix moa sfx when using custom music Also prevent fire and jump at the same time --- RandomizerCore/Asm/z2ft.s | 19 ++++++++++++++++++- RandomizerCore/Asm/z2mario/integration.s | 22 ++++++++++++++++++++++ RandomizerCore/ROM.cs | 4 ++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/RandomizerCore/Asm/z2ft.s b/RandomizerCore/Asm/z2ft.s index 429888efa..3d82594c2 100644 --- a/RandomizerCore/Asm/z2ft.s +++ b/RandomizerCore/Asm/z2ft.s @@ -25,7 +25,10 @@ UpdateSound = $878c jsr SwapPRG jsr UpdateSound - + ; clear out the moa fire work around + lda $07fd + and #~$40 + sta $07fd ; Noise channel buffer pla sta NmiBankShadowA sta PrgBankAReg @@ -50,3 +53,17 @@ UpdateSound = $878c ; Get rid of original call to UpdateSound to move it after setting up IRQ UPDATE_REFS SwapToPRG0 @ $c130 + +.segment "PRG6" +; Work around how moa fire works. It queues a noise sfx with writing $40 to $eb +; but doesn't set a buffer for it. This kept z2ft from knowing that the noise channel +; was in use, so we set the value in the buffer and clear it after update. +.org $95A3 + jmp FixMoaFireSfx +.reloc +FixMoaFireSfx: + sta $400C + lda $07fd + ora #$40 + sta $07fd ; Noise channel buffer + rts diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index e15a689fd..cc7b0a667 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -146,6 +146,28 @@ PreventTanookiSpellWhenSmallMario: @allow: rts +; "Table_for_Spell_effects" +; "its only a bit mask LUT" +; "SSSHH" +Table_for_Spell_effects = $8DBB +.org $8DFB + lda $076F + jsr ApplySpellEffectButPreventBothFireAndJump +.reloc +ApplySpellEffectButPreventBothFireAndJump: + ; apply the new spell flag + ora Table_for_Spell_effects,y + cpy #$04 ; if we are applying the fire spell + bne @notFire + and #~$02 ; clear the jump/tanooki bit + rts +@notFire: + cpy #1 ; jump/tanooki spell bit? + bne @apply + and #~$10 ; clear the fire bit +@apply: + rts + .org $8fe3 jmp *+3 ; Skip duplicate draw??? .reloc diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 314bac6c9..e523aa071 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -1320,6 +1320,10 @@ public void DisableFlashing() Put(0x2A03, 0x12); Put(0x1C9FA, 0x16); Put(0x1C9FC, 0x16); + // final flashing color is black for some reason in vanilla + // change this to red, the game will still put a black background + // next frame. this prevents a black flash when game overing + Put(0x1ca19, 0x16); } public async Task ApplyAsm(Assembler engine) From 96c0949fceebbe8f9ca9144769811cbeaadb9fad Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 28 May 2026 13:11:53 -0400 Subject: [PATCH 061/128] Add house to global music handler --- RandomizerCore/Music.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index 1e2a215e1..1e176d347 100644 --- a/RandomizerCore/Music.cs +++ b/RandomizerCore/Music.cs @@ -206,7 +206,7 @@ public void ImportSongs() { Usage.LastBoss, 1 }, { Usage.Credits, 1 }, { Usage.GameComplete, 1 }, - { Usage.House, 4 }, + { Usage.House, 1 }, }; var selUsesSongs = _imptr.SelectUsesSongs( usesSongs, numUsageSongs, _shuffler); @@ -315,6 +315,7 @@ SongMap CreateSongMap(UsesSongs usesSongs) { { Usage.Title, 0 }, { Usage.Overworld, 1 }, + { Usage.House, 7 }, { Usage.Boss, 0xb }, { Usage.LastBoss, 0x12 }, { Usage.Credits, 0x10 }, From 8dd1d76781eaae1cfa3db76a10bb251c5d95eba0 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 28 May 2026 13:40:10 -0400 Subject: [PATCH 062/128] Fix issues after rebase --- CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj | 2 +- CrossPlatformUI/Views/Tabs/SpellsView.axaml | 2 +- Directory.Packages.props | 1 + Tests/FlagsTests.cs | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj index 4bede0a2e..84d4299d0 100644 --- a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj +++ b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj @@ -93,7 +93,7 @@ - + diff --git a/CrossPlatformUI/Views/Tabs/SpellsView.axaml b/CrossPlatformUI/Views/Tabs/SpellsView.axaml index 7f19ccb2b..d39fb3574 100644 --- a/CrossPlatformUI/Views/Tabs/SpellsView.axaml +++ b/CrossPlatformUI/Views/Tabs/SpellsView.axaml @@ -76,7 +76,7 @@ diff --git a/Directory.Packages.props b/Directory.Packages.props index 10abacc93..064599182 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,7 @@ + diff --git a/Tests/FlagsTests.cs b/Tests/FlagsTests.cs index af39ddb85..787754304 100644 --- a/Tests/FlagsTests.cs +++ b/Tests/FlagsTests.cs @@ -207,7 +207,7 @@ public void SharedSeedExportIgnoresDifficultyOnlySettings() EnemyXPDrops = XPEffectiveness.NONE }; - RandomizerProperties properties = config.Export(new Random(1234), includeDifficulty: false); + RandomizerProperties properties = config.Export(new RandomizerCore.Random(1234), includeDifficulty: false); Assert.IsFalse(properties.StartCandle); Assert.IsFalse(properties.StartCross); From ffada8ae26672f1b4c1909eec625c8e53d6beeb2 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 28 May 2026 14:28:46 -0400 Subject: [PATCH 063/128] Fix up ram inc issues --- RandomizerCore/Asm/ram.inc | 349 +++++++++++++++++++++++++++++- RandomizerCore/Asm/z2mario/boss.s | 12 +- RandomizerCore/Asm/z2r.inc | 347 +---------------------------- RandomizerCore/ROM.cs | 3 +- 4 files changed, 358 insertions(+), 353 deletions(-) diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index 35e98e605..543e6d27f 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -1,3 +1,40 @@ +.ifndef _Z2R_RAMMAP_INC +_Z2R_RAMMAP_INC = 1 + + +; janky macro res till it starts working +.macro SET_RES_BASE addr + RES_BASE .set addr + RES_OFFSET .set 0 +.endmacro + +.macro RESV name, size + .ident(.string(name)) = RES_BASE + RES_OFFSET + .ifnblank size + RES_OFFSET .set RES_OFFSET + size + .else + RES_OFFSET .set RES_OFFSET + 1 + .endif +.endmacro + +; compile time array definition +.macro DEFINE_ARRAY name + .macro .ident(.concat(.string(name), "_APPEND")) value + .local temp + .define temp name + .undefine name + .ifblank temp + .define name value + .else + .define name temp, value + .endif + .undefine temp + END_MAC + + .define name +.endmacro +.define END_MAC .endmacro + ; Define low zero page addresses even if they're not well-defined ; so they will have debug info precedence over other constants zp_00 = $00 @@ -42,12 +79,14 @@ OverworldStepCounter = $26 zp_27 = $27 zp_28 = $28 LinkYPos = $29 +EnemyYPositionLo = $2a Enemy0YPositionLo = $2a Enemy1YPositionLo = $2b Enemy2YPositionLo = $2c Enemy3YPositionLo = $2d Enemy4YPositionLo = $2e Enemy5YPositionLo = $2f +ProjectileYPosition = $30 Projectile0YPosition = $30 Projectile1YPosition = $31 Projectile2YPosition = $32 @@ -60,6 +99,7 @@ zp_38 = $38 LinkProjectileYPositionRelated = $39 zp_3a = $3a LinkXPositionHi = $3b +EnemyXPositionHi = $3c Enemy0XPositionHi = $3c Enemy1XPositionHi = $3d Enemy2XPositionHi = $3e @@ -77,7 +117,9 @@ LinkProjectile1XPositionHi = $49 zp_4a = $4a LinkProjectileXPositionHiRelated = $4b zp_4c = $4c +LinkXPosition = $4d LinkXPositionLo = $4d +EnemyXPositionLo = $4e Enemy0XPositionLo = $4e Enemy1XPositionLo = $4f Enemy2XPositionLo = $50 @@ -135,6 +177,8 @@ OverworldEnemy1Type = $83 ; [SS] Enemy2AnimationFrame - [OW] Over OverworldEnemy2Type = $84 ; [SS] Enemy3AnimationFrame - [OW] OverworldEnemy2Type OverworldEnemy3Type = $85 ; [SS] Enemy4AnimationFrame - [OW] OverworldEnemy3Type OverworldEnemy4Type = $86 ; [SS] Enemy5AnimationFrame - [OW] OverworldEnemy4Type +ProjectileType = $87 +Enemy_Flag = ProjectileType Projectile0Type = $87 ; [SS] Projectile0Type - [OW] OverworldEnemy5Type Projectile1Type = $88 ; [SS] Projectile1Type - [OW] OverworldEnemy6Type Projectile2Type = $89 ; [SS] Projectile2Type - [OW] OverworldEnemy7Type @@ -161,6 +205,7 @@ SpriteShuffleOffsetLinkProjectile0 = $9d SpriteShuffleOffsetLinkProjectile1 = $9e DpadHorizontalState = $9f ; 1 = Right, 2 = Left zp_a0 = $a0 +EnemyType = $a1 Enemy0Type = $a1 ; Enemy (Elevator) slot Enemy1Type = $a2 ; Enemy (Locked door) slot Enemy2Type = $a3 @@ -194,6 +239,7 @@ Enemy2Persistence = $be Enemy3Persistence = $bf Enemy4Persistence = $c0 Enemy5Persistence = $c1 +EnemyHP = $c2 Enemy0HP = $c2 Enemy1HP = $c3 Enemy2HP = $c4 @@ -226,6 +272,13 @@ zp_de = $de zp_df = $df ; Enemy type temporary storage ; $e0-$ef are music and sound variables +Z2NoiseSoundQueue = $ed +Z2Square2SoundQueue = $ee +Z2Square1SoundQueue = $ef + +NoiseSoundQueue = $f0 +Square2SoundQueue = $f1 +Square1SoundQueue = $f2 ; Potentially free zero page memory bytes at $f1-$f4 and since they are used by FDS BIOS but not the NES Controller1ButtonsPressed = $f5 @@ -236,6 +289,16 @@ Controller2ButtonsHeld = $f8 ScrollPosShadow = $fd PpuCtrlShadow = $ff + +Sprite_Y_Position = $200 +Sprite_Tilenumber = $201 +Sprite_Attributes = $202 +Sprite_X_Position = $203 +Sprite_Data = Sprite_Y_Position + +VRAM_Buffer1_Offset = $302 +VRAM_Buffer1 = $303 + LinkXSubpixel = $03d6 Enemy0XSubpixel = $03d7 Enemy1XSubpixel = $03d8 @@ -245,6 +308,18 @@ Enemy4XSubpixel = $03db Enemy5XSubpixel = $03dc LinkYVelocityLo = $03e6 +; This is where the buffers for breaking blocks are stored in vanilla. We increase the number of blocks that can be broken +; at the same time by expanding the number of blocks here, and then moving the extra fields to a different location in +; stack ram +; Expanded the block break list to hold up to 10 bricks at a time instead of just 5 +BREAKABLE_BLOCK_COUNT = 10 +RelocatedRAMBase = $041A +SET_RES_BASE RelocatedRAMBase +RESV BlockBreakState, BREAKABLE_BLOCK_COUNT ; Vanilla addr $041a - bits 0-6 = timer; bit 7 set = step block. $00 means no brick breaking +RESV BlockBreakMapPage, BREAKABLE_BLOCK_COUNT ; Vanilla addr $041F - Nametable select for the PPUADDR +RESV BlockBreakXCoord, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0424 - X Low Byte for collision table +.assert RES_BASE + RES_OFFSET < $043E + LinkSwordAttackFrame = $0400 Enemy0HitState = $040e LinkInAir = $0479 ; 0 = On ground, 1 = Falling from walking off platform, 2 = Jumping @@ -256,6 +331,7 @@ LinkIFrameTimer = $500 timer_501 = $0501 ; $501 through $50d are timers that are decreased each frame until 0 LinkEntrySlideTime = $0503 SwordAttackTimer = $050a +InjuryTimer = $050c EncounterSpawnTimer = $0516 RNG = $051b MapNumber = $0561 @@ -263,6 +339,7 @@ TownNumber = $056b PalaceNumber = $056c PalaceRegionIndex = $056c ; Palace index in continent (0-2) LinkYVelocityHi = $057d +EnemyYVelocity = $057e Enemy0YVelocity = $057e LinkProjectile0Timer = $05ca LinkProjectile1Timer = $05cb ; Always 0 since Fire projectiles don't expire @@ -274,6 +351,7 @@ SmallDropCount = $05df LargeDropCount = $05e0 Lives = $0700 +NumberofLives = $0700 EntryFacing = $0701 ; Sideview entry facing direction. 0=Facing right (enter from the left), 1=Facing left (enter from the right) EnterFromElevatorAbove = $0704 ; >0 Link enters the room from the top EnterCentered = $0705 ; >0 Link enters centered; like at North Palace, taking an elevator exit or entering a random encounter @@ -284,7 +362,14 @@ ColumnScrollDirection = $0720 ; Scroll direction that is updated only ColumnScrollX = $0721 ; Scroll position within the current tile column ($00-$1f) DialogBoxDrawing = $0726 ; >0 when drawing dialog and during black screen transitions ScrollFrozen = $0728 ; 1=Screen scrolling frozen (for boss fights) +ScrollLock = $0728 +ScreenLeft_PageLoc = $72a ScrollPage = $072a ; Which page the left-most pixel of the screen is on (0-3) +ScreenRight_PageLoc = $72b +ScreenEdge_PageLoc = ScreenLeft_PageLoc +ScreenLeft_X_Pos = $72c +ScreenRight_X_Pos = $72d +ScreenEdge_X_Pos = ScreenLeft_X_Pos ScrollX = $072c SideviewCommandY = $0730 SideviewCommandOp = $0731 @@ -311,7 +396,7 @@ EnterAreaFlag = $075f ; Set to 1 when entering a screen cente CurrentPrgBank = $0769 GameEvent = $076c ; 0=Restart with full lives, 1=Main game loop, 2=Link dying, 3=Wake up Zelda, 4=Roll credits, 06=Show lives left then restart the scene GameEventCurrent = $076d ; GameEvent is compared to this and only triggered if it has changed -CurrentChrBank = $076e +CurrentCHRBank = $076e FireSpellActive = $076f ; 0x10 = Fire spell active ExpForNextLevelHi = $0770 ExpForNextLevelLo = $0771 @@ -352,8 +437,270 @@ HaveChild = $079c ; 0x20 = Have Child HaveBasementAccess = $079d ; 0x08 = Have New Kasuto Basement access ContinuesUsed = $079f +; These are new variables that use free memory +ProjectileEnemyData = $07c0 +Z2Noise_SfxLenCounter = $07f5 +Z2Squ2_SfxLenCounter = $07f7 +Z2Squ1_SfxLenCounter = $07f9 +Z2NoiseSoundBuffer = $07fd +Z2Square2SoundBuffer = $07fe +Z2Square1SoundBuffer = $07ff SavedDoorExit1ScrollPage = $69aa ; accessed as $69a5,X with X = 5 SavedDoorExit1ScrollX = $69b1 ; accessed as $69ac,X with X = 5 SavedDoorExit2ScrollPage = $69ab ; used for New Kasuto fireplace SavedDoorExit2ScrollX = $69b2 + +; All Timestamp types listed below for reference +TS_COUNT .set 0 +.macro TimestampId name +.ident(.string(name)) = TS_COUNT +TS_COUNT .set TS_COUNT + 1 +.endmacro + +TimestampId TsGlove +TimestampId TsRaft +TimestampId TsBoots +TimestampId TsHammer +TimestampId TsJumpSpell +TimestampId TsFairySpell +TimestampId TsReflectSpell +TimestampId TsThunderSpell +TimestampId TsPalace1 +TimestampId TsPalace2 +TimestampId TsPalace3 +TimestampId TsPalace4 +TimestampId TsPalace5 +TimestampId TsPalace6 +TimestampId TsPalaceGP +TimestampId TsTowns + + +;; NEW VARIABLE ALLOCATIONS +; Stack RAM +StackRAMBase = $0120 +SET_RES_BASE StackRAMBase +RESV temp_room_code ; , $0120 +RESV temp_room_flag ; , $0121 +RESV DontKillEnemyFlag ; , $0122 +RESV Global5050JarDrop ; = $123 ; $0123 +RESV PreventDoubleLag +RESV PreviousFrameMetasprite +RESV StatDisplayState +RESV InternalState +RESV GameComplete ; set when you beat the game to stop the timers +RESV ElevatorYStart +RESV DripperRedCounter +RESV PpuCtrlForIrq +RESV ScrollPosForIrq +RESV IrqSetupAlready +RESV ProjectileProcessing +RESV PendingAudioCall ; 1=audio running on main thread, 0=NMI decremented it (needs re-run) +RESV BlockBreakYCoord, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0429 - Y Low byte for collision table +RESV BlockBreakColumnOffset, BREAKABLE_BLOCK_COUNT ; Vanilla addr $042E - Offset in the collision table +RESV BlockBreakAddrLo, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table lo addr +RESV BlockBreakAddrHi, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table hi addr + + +.ifdef ENABLE_Z2_MARIO + +; New RAM values that aren't from vanilla SMB1 code +RESV mapMarioIndex +RESV mapMarioTimer +RESV mapMarioPrevDirection + +;;; +; MARIO related values that are not directly mapped to vanilla z2 object values + +ObjectOffset = $10 +; RESV FBall_OffscreenBits +; RESV AltEntranceControl +; RESV PlayerEntranceCtrl +; RESV JoypadOverride +; RESV Vine_Height +RESV Player_State +; RESV DisableCollisionDet +RESV GameEngineSubroutine +; RESV AreaType +; RESV A_B_Buttons +A_B_Buttons = $741 +RESV PreviousA_B_Buttons +; RESV Left_Right_Buttons +Left_Right_Buttons = $742 +; RESV Up_Down_Buttons +Up_Down_Buttons = $743 +RESV PlayerSize +; RESV DeathMusicLoaded +RESV EventMusicQueue +RESV EventMusicBuffer ; TODO use the vanilla music queues +; RESV WarpZoneControl +RESV Player_CollisionBits ; Split Z2 and SMB1 collision bits +; RESV HalfwayPage +;RESV SwimmingFlag +SwimmingFlag = $13 +RESV CurrentOAMOffset +; RESV ObjectMetasprite +RESV PlayerAnimCtrl +RESV PlayerChangeSizeFlag +RESV VerticalForceDown +RESV VerticalForce +RESV JumpOrigin_Y_Position +RESV JumpOrigin_Y_HighPos +RESV DiffToHaltJump +RESV PlayerAnimTimerSet +RESV JumpspringAnimCtrl +RESV Player_XSpeedAbsolute +; RESV Whirlpool_Flag +RESV RunningSpeed +RESV MaximumLeftSpeed +RESV MaximumRightSpeed +RESV FrictionAdderLow +RESV FrictionAdderHigh +RESV Player_Pos_ForScroll +RESV Player_X_Scroll +RESV Player_X_MoveForce +RESV PlayerStandingMetatile ; added to keep this for door checks and other metatiles +RESV PlayerIsDownstabbing ; Simplify downstab checks by just storing if we set the hitbox + +; Non-timer timer. +RESV FireballThrowingTimer + +RESV TimerControl +RESV IntervalTimerControl + +Timers = PlayerAnimTimer +RESV PlayerAnimTimer +RESV ClimbSideTimer +RESV JumpSwimTimer +RESV RunningTimer +RESV SideCollisionTimer +; RESV BlockBounceTimer +FRAME_TIMER_COUNT = SideCollisionTimer - Timers +; RESV StarInvincibleTimer +ALL_TIMER_COUNT = SideCollisionTimer - Timers ; StarInvincibleTimer - Timers + +RESV SpriteShuffleOffset +;RESV CurrentCHRBank +RESV PlayerChrBank +RESV ReloadCHRBank +RESV ObjectVerticalFlip +RESV Local_eb +; Used by hammers to track how long they've been alive +RESV PlayerProj_Cnt, 2 + +; Values use for SMB1 sfx audio engine + +RESV Sfx_SecondaryCounter +; RESV PauseSoundBuffer +; RESV PauseModeFlag +; RESV PauseSoundQueue +RESV Square1SoundBuffer +RESV Square2SoundBuffer +RESV NoiseSoundBuffer +RESV Squ1_SfxLenCounter +RESV Squ2_SfxLenCounter +RESV Noise_SfxLenCounter +RESV TailWag_CtlHold +RESV TailWag_FreqLo +RESV TailWag_FreqHi + +RESV JustThrownHammer +RESV TailSwingTimer +; SMB3 raccoon flutter: nonzero = Y-velocity cap active +RESV TailWagCount +; Per-enemy "hit by hammer" flag. Persistent across the enemy's hit-flash so +; later per-enemy recoil setters ($9AF9 etc., which run after ProjectileProcessing +; is cleared) can still know to suppress the knockback. Cleared when the flash +; timer at $040E,x decrements to zero (see bank7_Display patch). +RESV HammerHitFlag, 6 + +; Shadow Mario Boss RAM +CUSTOM_BOSS_START = RES_OFFSET +RESV boss_animation +RESV boss_invincibility_timer +RESV boss_screech +RESV boss_state +RESV boss_state_timer +RESV boss_walking_anim +RESV boss_bridge +RESV boss_shootanim +RESV boss_substate +RESV boss_counter +RESV boss_freeze +RESV boss_ChrBank +RESV boss_transition_delay ; nonzero = boss is paused mid-state-switch (frames remaining) +BOSS_RAM_SIZE = RES_OFFSET - CUSTOM_BOSS_START - 1 + +EnemyProjectileOffset = 7 +EnemyProjectileType = ObjectMetasprite + EnemyProjectileOffset +EnemyProjectile_X_Position = SprObject_X_Position + EnemyProjectileOffset +EnemyProjectile_Y_Position = SprObject_Y_Position + EnemyProjectileOffset +EnemyProjectile_PageLoc = SprObject_PageLoc + EnemyProjectileOffset +EnemyProjectile_Y_HighPos = SprObject_Y_HighPos + EnemyProjectileOffset +EnemyProjectile_X_Speed = SprObject_X_Speed + EnemyProjectileOffset +EnemyProjectile_Y_Speed = SprObject_Y_Speed + EnemyProjectileOffset +EnemyProjectile_SprAttrib = SprObject_SprAttrib + EnemyProjectileOffset +ENEMY_PROJECTILE_COUNT = 6 + +; MARIO SPECIFIC RAM LOCATIONS +; These are values that are hardcoded to match where Zelda 2 places them +Player_X_Speed = $70 +SprObject_X_Speed = Player_X_Speed +Player_X_Position = $4D +SprObject_X_Position = Player_X_Position +Player_PageLoc = $3B +SprObject_PageLoc = Player_PageLoc +Player_Y_Position = $29 +SprObject_Y_Position = Player_Y_Position +Player_Y_HighPos = $19 +SprObject_Y_HighPos = Player_Y_HighPos + +;Player_CollisionBits = $A7 +ScrollAmount = $14 +ObjectMetasprite = $80 + +ProjectileOffset = 13 +FireballOffset = 13 +FireballMetasprite = ObjectMetasprite + FireballOffset +Fireball_X_Position = SprObject_X_Position + FireballOffset +Fireball_Y_Position = SprObject_Y_Position + FireballOffset +Fireball_Y_HighPos = SprObject_Y_HighPos + FireballOffset +Fireball_PageLoc = SprObject_PageLoc + FireballOffset +Fireball_X_Speed = SprObject_X_Speed + FireballOffset +Fireball_Y_Speed = SprObject_Y_Speed + FireballOffset +RESV FireballBouncingFlag, 2 +RESV Fireball_State, 2 + +EnemyOffset = 1 ; Enemy slot 0 = offset 1 from player +Enemy_X_Position = SprObject_X_Position + EnemyOffset +Enemy_Y_Position = SprObject_Y_Position + EnemyOffset +Enemy_X_Speed = SprObject_X_Speed + EnemyOffset +Enemy_PageLoc = SprObject_PageLoc + EnemyOffset +Enemy_Y_HighPos = SprObject_Y_HighPos + EnemyOffset +Enemy_MovingDir = Player_MovingDir + EnemyOffset +Enemy_State = $A8 + + +; For reasons I cannot explain, player_x_moveforce is not the same as sprobject_x_moveforce +SprObject_X_MoveForce = $03d6 ; Player_X_MoveForce +Player_Y_MoveForce = $03e6 +SprObject_Y_MoveForce = Player_Y_MoveForce +Enemy_X_MoveForce = SprObject_X_MoveForce + EnemyOffset +Enemy_Y_MoveForce = SprObject_Y_MoveForce + EnemyOffset + +Player_Y_Speed = $057D +SprObject_Y_Speed = Player_Y_Speed +Enemy_Y_Speed = SprObject_Y_Speed + EnemyOffset + +; CrouchingFlag = $17 +RESV CrouchingFlag +Player_MovingDir = $5F +Fireball_MovingDir = $6D +PlayerFacingDir = $9F +EnemyFacingDir = $A0 +SavedJoypadBits = $F7 + +.assert RES_BASE + RES_OFFSET < $01bd +.endif ; ENABLE_Z2_MARIO + +.endif ; _Z2R_RAMMAP_INC \ No newline at end of file diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index 9dd0c02fb..c691074e1 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -117,7 +117,7 @@ InitShadowBoss: ldx #0 ; Dark Link entity is always at enemy slot 0 ; Set HP lda #60 - sta Enemy_HP,x + sta EnemyHP,x lda #JUMPING sta boss_animation lda #CHR_BIGMARIO @@ -201,7 +201,7 @@ RunBoss: rts @notfreezed: - lda Enemy_HP,x + lda EnemyHP,x bmi AnimateKilledBoss ; HP < 0: boss is fully dead bne @alive jmp KillBoss ; HP == 0: playing death countdown @@ -1065,7 +1065,7 @@ KillBoss: jsr MoveD_EnemyVertically ; gravity decelerates upward motion then pulls boss down dec boss_counter bne @done ; still in animation window - dec Enemy_HP,x + dec EnemyHP,x @done: ldx #0 rts @@ -1090,7 +1090,7 @@ InjuredBoss: lda boss_invincibility_timer bne @done ; already invincible, no damage inc boss_screech ; track hit count - dec Enemy_HP,x ; reduce HP + dec EnemyHP,x ; reduce HP lda #80 sta boss_invincibility_timer ; 80 frames of invincibility @done: @@ -1149,13 +1149,13 @@ InjuredBossJump: lda boss_invincibility_timer bne @done ; already invincible, no damage (caller still bounces Mario) inc boss_screech - lda Enemy_HP,x + lda EnemyHP,x sec sbc #5 bcs + lda #0 ; floor at 0 + - sta Enemy_HP,x + sta EnemyHP,x beq + ; don't play sfx for last hit. works around a weird bug i didn't bother to fix lda #$10 ; Sword stab SFX diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index fef51fac4..54215276c 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -3,39 +3,6 @@ _Z2R_INC = 1 .macpack common -; janky macro res till it starts working -.macro SET_RES_BASE addr - RES_BASE .set addr - RES_OFFSET .set 0 -.endmacro - -.macro RESV name, size - .ident(.string(name)) = RES_BASE + RES_OFFSET - .ifnblank size - RES_OFFSET .set RES_OFFSET + size - .else - RES_OFFSET .set RES_OFFSET + 1 - .endif -.endmacro - -; compile time array definition -.macro DEFINE_ARRAY name - .macro .ident(.concat(.string(name), "_APPEND")) value - .local temp - .define temp name - .undefine name - .ifblank temp - .define name value - .else - .define name temp, value - .endif - .undefine temp - END_MAC - - .define name -.endmacro -.define END_MAC .endmacro - ;;; Standard register names PPUCTRL = $2000 PPUMASK = $2001 @@ -53,6 +20,7 @@ JOY2 = $4017 ;;; Include RAM values before any constants .include "ram.inc" + R0 = $00 R1 = $01 R2 = $02 @@ -132,288 +100,11 @@ EnemyFacingDirection = $DC91 LoadPlayerShieldCollisionBox = $e9d8 ObjectCollisionCheck = $e9f9 MoveEnemyHorizontally = $deb8 - -; These are new variables that use free memory -Z2Noise_SfxLenCounter = $07f5 -Z2Squ2_SfxLenCounter = $07f7 -Z2Squ1_SfxLenCounter = $07f9 -Z2NoiseSoundBuffer = $07fd -Z2Square2SoundBuffer = $07fe -Z2Square1SoundBuffer = $07ff - -Z2NoiseSoundQueue = $ed -Z2Square2SoundQueue = $ee -Z2Square1SoundQueue = $ef - -NoiseSoundQueue = $f0 -Square2SoundQueue = $f1 -Square1SoundQueue = $f2 - -ProjectileYPosition = $30 -ProjectileXVelocity = $77 -ProjectileType = $87 -EnemyType = $a1 -Enemy_Flag = ProjectileType -LinkXPosition = $4d -EnemyXPositionLo = $4e -EnemyXPositionHi = $3c -Enemy_HP = $c2 -RNG = $051b -ProjectileEnemyData = $07c0 -EnemyYVelocity = $057e - -;; NEW VARIABLE ALLOCATIONS -; Stack RAM -StackRAMBase = $0120 -SET_RES_BASE StackRAMBase -RESV temp_room_code ; , $0120 -RESV temp_room_flag ; , $0121 -RESV DontKillEnemyFlag ; , $0122 -RESV Global5050JarDrop ; = $123 ; $0123 -RESV PreventDoubleLag -RESV PreviousFrameMetasprite -RESV StatDisplayState -RESV InternalState -RESV GameComplete ; set when you beat the game to stop the timers -RESV ElevatorYStart -RESV DripperRedCounter -RESV PpuCtrlForIrq -RESV ScrollPosForIrq -RESV IrqSetupAlready -RESV ProjectileProcessing -RESV PendingAudioCall ; 1=audio running on main thread, 0=NMI decremented it (needs re-run) -; Expanded the block break list to hold up to 10 bricks at a time instead of just 5 -BREAKABLE_BLOCK_COUNT = 10 -RESV BlockBreakYCoord, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0429 - Y Low byte for collision table -RESV BlockBreakColumnOffset, BREAKABLE_BLOCK_COUNT ; Vanilla addr $042E - Offset in the collision table -RESV BlockBreakAddrLo, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table lo addr -RESV BlockBreakAddrHi, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table hi addr - - -.ifdef ENABLE_Z2_MARIO - -; New RAM values that aren't from vanilla SMB1 code -RESV mapMarioIndex -RESV mapMarioTimer -RESV mapMarioPrevDirection - -;;; -; MARIO related values that are not directly mapped to vanilla z2 object values - -ObjectOffset = $10 -; RESV FBall_OffscreenBits -; RESV AltEntranceControl -; RESV PlayerEntranceCtrl -; RESV JoypadOverride -; RESV Vine_Height -RESV Player_State -; RESV DisableCollisionDet -RESV GameEngineSubroutine -; RESV AreaType -; RESV A_B_Buttons -A_B_Buttons = $741 -RESV PreviousA_B_Buttons -; RESV Left_Right_Buttons -Left_Right_Buttons = $742 -; RESV Up_Down_Buttons -Up_Down_Buttons = $743 -RESV PlayerSize -; RESV DeathMusicLoaded -RESV EventMusicQueue -RESV EventMusicBuffer ; TODO use the vanilla music queues -; RESV WarpZoneControl -RESV Player_CollisionBits ; Split Z2 and SMB1 collision bits -; RESV HalfwayPage -;RESV SwimmingFlag -SwimmingFlag = $13 -RESV CurrentOAMOffset -; RESV ObjectMetasprite -RESV PlayerAnimCtrl -RESV PlayerChangeSizeFlag -RESV VerticalForceDown -RESV VerticalForce -RESV JumpOrigin_Y_Position -RESV JumpOrigin_Y_HighPos -RESV DiffToHaltJump -RESV PlayerAnimTimerSet -RESV JumpspringAnimCtrl -RESV Player_XSpeedAbsolute -; RESV Whirlpool_Flag -RESV RunningSpeed -RESV MaximumLeftSpeed -RESV MaximumRightSpeed -RESV FrictionAdderLow -RESV FrictionAdderHigh -RESV Player_Pos_ForScroll -RESV Player_X_Scroll -RESV Player_X_MoveForce -RESV PlayerStandingMetatile ; added to keep this for door checks and other metatiles -RESV PlayerIsDownstabbing ; Simplify downstab checks by just storing if we set the hitbox - -; Non-timer timer. -RESV FireballThrowingTimer - -RESV TimerControl -RESV IntervalTimerControl - -Timers = PlayerAnimTimer -RESV PlayerAnimTimer -RESV ClimbSideTimer -RESV JumpSwimTimer -RESV RunningTimer -RESV SideCollisionTimer -; RESV BlockBounceTimer -FRAME_TIMER_COUNT = SideCollisionTimer - Timers -; RESV StarInvincibleTimer -ALL_TIMER_COUNT = SideCollisionTimer - Timers ; StarInvincibleTimer - Timers - -RESV SpriteShuffleOffset -;RESV CurrentCHRBank -RESV PlayerChrBank -RESV ReloadCHRBank -RESV ObjectVerticalFlip -RESV Local_eb -; Used by hammers to track how long they've been alive -RESV PlayerProj_Cnt, 2 - -; Values use for SMB1 sfx audio engine - -RESV Sfx_SecondaryCounter -; RESV PauseSoundBuffer -; RESV PauseModeFlag -; RESV PauseSoundQueue -RESV Square1SoundBuffer -RESV Square2SoundBuffer -RESV NoiseSoundBuffer -RESV Squ1_SfxLenCounter -RESV Squ2_SfxLenCounter -RESV Noise_SfxLenCounter -RESV TailWag_CtlHold -RESV TailWag_FreqLo -RESV TailWag_FreqHi - -RESV JustThrownHammer -RESV TailSwingTimer -; SMB3 raccoon flutter: nonzero = Y-velocity cap active -RESV TailWagCount -; Per-enemy "hit by hammer" flag. Persistent across the enemy's hit-flash so -; later per-enemy recoil setters ($9AF9 etc., which run after ProjectileProcessing -; is cleared) can still know to suppress the knockback. Cleared when the flash -; timer at $040E,x decrements to zero (see bank7_Display patch). -RESV HammerHitFlag, 6 - -; Shadow Mario Boss RAM -CUSTOM_BOSS_START = RES_OFFSET -RESV boss_animation -RESV boss_invincibility_timer -RESV boss_screech -RESV boss_state -RESV boss_state_timer -RESV boss_walking_anim -RESV boss_bridge -RESV boss_shootanim -RESV boss_substate -RESV boss_counter -RESV boss_freeze -RESV boss_ChrBank -RESV boss_transition_delay ; nonzero = boss is paused mid-state-switch (frames remaining) -BOSS_RAM_SIZE = RES_OFFSET - CUSTOM_BOSS_START - 1 - -EnemyProjectileOffset = 7 -EnemyProjectileType = ObjectMetasprite + EnemyProjectileOffset -EnemyProjectile_X_Position = SprObject_X_Position + EnemyProjectileOffset -EnemyProjectile_Y_Position = SprObject_Y_Position + EnemyProjectileOffset -EnemyProjectile_PageLoc = SprObject_PageLoc + EnemyProjectileOffset -EnemyProjectile_Y_HighPos = SprObject_Y_HighPos + EnemyProjectileOffset -EnemyProjectile_X_Speed = SprObject_X_Speed + EnemyProjectileOffset -EnemyProjectile_Y_Speed = SprObject_Y_Speed + EnemyProjectileOffset -EnemyProjectile_SprAttrib = SprObject_SprAttrib + EnemyProjectileOffset -ENEMY_PROJECTILE_COUNT = 6 - -; MARIO SPECIFIC RAM LOCATIONS -; These are values that are hardcoded to match where Zelda 2 places them -Player_X_Speed = $70 -SprObject_X_Speed = Player_X_Speed -Player_X_Position = $4D -SprObject_X_Position = Player_X_Position -Player_PageLoc = $3B -SprObject_PageLoc = Player_PageLoc -Player_Y_Position = $29 -SprObject_Y_Position = Player_Y_Position -Player_Y_HighPos = $19 -SprObject_Y_HighPos = Player_Y_HighPos - -;Player_CollisionBits = $A7 -ScrollAmount = $14 -ObjectMetasprite = $80 - -ProjectileOffset = 13 -FireballOffset = 13 -FireballMetasprite = ObjectMetasprite + FireballOffset -Fireball_X_Position = SprObject_X_Position + FireballOffset -Fireball_Y_Position = SprObject_Y_Position + FireballOffset -Fireball_Y_HighPos = SprObject_Y_HighPos + FireballOffset -Fireball_PageLoc = SprObject_PageLoc + FireballOffset -Fireball_X_Speed = SprObject_X_Speed + FireballOffset -Fireball_Y_Speed = SprObject_Y_Speed + FireballOffset -RESV FireballBouncingFlag, 2 -RESV Fireball_State, 2 - -EnemyOffset = 1 ; Enemy slot 0 = offset 1 from player -Enemy_X_Position = SprObject_X_Position + EnemyOffset -Enemy_Y_Position = SprObject_Y_Position + EnemyOffset -Enemy_X_Speed = SprObject_X_Speed + EnemyOffset -Enemy_PageLoc = SprObject_PageLoc + EnemyOffset -Enemy_Y_HighPos = SprObject_Y_HighPos + EnemyOffset -Enemy_MovingDir = Player_MovingDir + EnemyOffset -Enemy_State = $A8 - - -; For reasons I cannot explain, player_x_moveforce is not the same as sprobject_x_moveforce -SprObject_X_MoveForce = $03d6 ; Player_X_MoveForce -Player_Y_MoveForce = $03e6 -SprObject_Y_MoveForce = Player_Y_MoveForce -Enemy_X_MoveForce = SprObject_X_MoveForce + EnemyOffset -Enemy_Y_MoveForce = SprObject_Y_MoveForce + EnemyOffset - -Player_Y_Speed = $057D -SprObject_Y_Speed = Player_Y_Speed -Enemy_Y_Speed = SprObject_Y_Speed + EnemyOffset - -FrameCounter = $12 -; CrouchingFlag = $17 -RESV CrouchingFlag -Player_MovingDir = $5F -Fireball_MovingDir = $6D -PlayerFacingDir = $9F -EnemyFacingDir = $A0 -SavedJoypadBits = $F7 -ScreenLeft_PageLoc = $72a -ScreenRight_PageLoc = $72b -ScreenEdge_PageLoc = ScreenLeft_PageLoc -ScreenLeft_X_Pos = $72c -ScreenRight_X_Pos = $72d -ScreenEdge_X_Pos = ScreenLeft_X_Pos - -InjuryTimer = $050c -NumberofLives = $0700 -; Player_SprAttrib = $0752 -; SprObject_SprAttrib = Player_SprAttrib -ScrollLock = $0728 - MoveObjectHorizontally = $D1CE CheckForSolidMTiles = $850C CheckSideviewTransition = $E14A -VRAM_Buffer1_Offset = $302 -VRAM_Buffer1 = $303 - -Sprite_Y_Position = $200 -Sprite_Tilenumber = $201 -Sprite_Attributes = $202 -Sprite_X_Position = $203 -Sprite_Data = Sprite_Y_Position - +.ifdef ENABLE_Z2_MARIO ;; Constants CHR_BIGMARIO = $6a @@ -472,16 +163,6 @@ DeathMusic = %00000001 .endif ; ENABLE_Z2_MARIO -.assert RES_BASE + RES_OFFSET < $01bd - - -RelocatedRAMBase = $041A -SET_RES_BASE RelocatedRAMBase -RESV BlockBreakState, BREAKABLE_BLOCK_COUNT ; Vanilla addr $041a - bits 0-6 = timer; bit 7 set = step block. $00 means no brick breaking -RESV BlockBreakMapPage, BREAKABLE_BLOCK_COUNT ; Vanilla addr $041F - Nametable select for the PPUADDR -RESV BlockBreakXCoord, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0424 - X Low Byte for collision table -.assert RES_BASE + RES_OFFSET < $043E - ;; Stat tracking variables in SRAM ; This value is the second header byte for file 1 thats unused. @@ -508,30 +189,6 @@ SET_RES_BASE StatTrackingBase ;; The follow stats are NOT checkpointed because they persist between reloads -; All Timestamp types listed below for reference -TS_COUNT .set 0 -.macro TimestampId name -.ident(.string(name)) = TS_COUNT -TS_COUNT .set TS_COUNT + 1 -.endmacro - -TimestampId TsGlove -TimestampId TsRaft -TimestampId TsBoots -TimestampId TsHammer -TimestampId TsJumpSpell -TimestampId TsFairySpell -TimestampId TsReflectSpell -TimestampId TsThunderSpell -TimestampId TsPalace1 -TimestampId TsPalace2 -TimestampId TsPalace3 -TimestampId TsPalace4 -TimestampId TsPalace5 -TimestampId TsPalace6 -TimestampId TsPalaceGP -TimestampId TsTowns - ; All of the values prefixed with Stat are persisted across resets and Continue without saves ; Overall gameplay timer ticks every NMI and never stops diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index e523aa071..55f3c03df 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -1296,6 +1296,7 @@ public void UpdateMapPointers() public void UpAController1(Assembler a) { a.Module().Code(""" +.include "z2r.inc" .segment "PRG0" .org $a19f CheckController1ForUpAUnknown: @@ -2086,7 +2087,7 @@ sta SpChrBank4Reg adc #1 sta SpChrBank5Reg ; due to an MMC5 issue, we need to write a bg bank as well. - lda CurrentChrBank + lda CurrentCHRBank asl asl ; clc ; carry is clear here From 0d61515a2ccf27284317e74a79241ad45f190dab Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 29 May 2026 00:58:10 -0400 Subject: [PATCH 064/128] Add original ftm modules for the z2mario music tracks --- RandomizerCore/Asm/z2mario/music/Battle.dnm | Bin 0 -> 21433 bytes .../Asm/z2mario/music/BattleSwing.dnm | Bin 0 -> 24815 bytes RandomizerCore/Asm/z2mario/music/Boss.dnm | Bin 0 -> 16728 bytes RandomizerCore/Asm/z2mario/music/Cave.ftm | Bin 0 -> 13010 bytes RandomizerCore/Asm/z2mario/music/Credits.ftm | Bin 0 -> 13864 bytes RandomizerCore/Asm/z2mario/music/FinalBoss.dnm | Bin 0 -> 16751 bytes .../Asm/z2mario/music/GreatPalace.dnm | Bin 0 -> 23836 bytes RandomizerCore/Asm/z2mario/music/Indoors.dnm | Bin 0 -> 14124 bytes RandomizerCore/Asm/z2mario/music/Overworld.dnm | Bin 0 -> 28694 bytes RandomizerCore/Asm/z2mario/music/Palace.dnm | Bin 0 -> 28420 bytes .../Asm/z2mario/music/TitleScreen.ftm | Bin 0 -> 26540 bytes RandomizerCore/Asm/z2mario/music/Town.dnm | Bin 0 -> 19331 bytes RandomizerCore/Asm/z2mario/music/ZeldaSaved.ftm | Bin 0 -> 3823 bytes 13 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 RandomizerCore/Asm/z2mario/music/Battle.dnm create mode 100644 RandomizerCore/Asm/z2mario/music/BattleSwing.dnm create mode 100644 RandomizerCore/Asm/z2mario/music/Boss.dnm create mode 100644 RandomizerCore/Asm/z2mario/music/Cave.ftm create mode 100644 RandomizerCore/Asm/z2mario/music/Credits.ftm create mode 100644 RandomizerCore/Asm/z2mario/music/FinalBoss.dnm create mode 100644 RandomizerCore/Asm/z2mario/music/GreatPalace.dnm create mode 100644 RandomizerCore/Asm/z2mario/music/Indoors.dnm create mode 100644 RandomizerCore/Asm/z2mario/music/Overworld.dnm create mode 100644 RandomizerCore/Asm/z2mario/music/Palace.dnm create mode 100644 RandomizerCore/Asm/z2mario/music/TitleScreen.ftm create mode 100644 RandomizerCore/Asm/z2mario/music/Town.dnm create mode 100644 RandomizerCore/Asm/z2mario/music/ZeldaSaved.ftm diff --git a/RandomizerCore/Asm/z2mario/music/Battle.dnm b/RandomizerCore/Asm/z2mario/music/Battle.dnm new file mode 100644 index 0000000000000000000000000000000000000000..e4f044b289396be8c9e38bc5613813da8bd6a818 GIT binary patch literal 21433 zcmeI3dygE&6~+58@7V_|U>+g3aaIXIKo*FiC?bg;tb>(!acuK{R(K^=7#pqEO8&^_ z$T!P(K_P;i-#t~mGqdZCohSmuU3hkGRo%LE>zsS5x_g=r@4bC>|BJ)hNBf_CesI)W zzklc9-Gdv0qPTJC=B4Yminfd-{gB$QZ7PaVvUH1Q^l9|Dws-YoeYJ)DKee_0o$%4_ zr4M&+p4hJ?Zc)9we|&uRK#rD0Rn+qJ+TN|(H$S<)yLa1bV_AQHvnVd0tx_&7kA`pi ze;o)+h0vv=FPqDE_wRiU+kszMft)IejUK-)1ZG0%^8SMd&5eh59~=Oi0R-1nx`y}g zlCBYaq`a{kmn3zR9=k3C!0yV?{)5lh!j0X$oEW)2vbqrHP^-&zH(5`fsfKP zxbG!>e}DMt=gr>z!v_a|nd5H_@F+ciT?hbK0C@Z8kR8vv-+I}@*M(XeRlt*e;hWd^|rN^)f0gjIwcOU97ey`ctP{lF$O38Ii!QZaY zeJ^)!HxKU|aOG_1YObGMdg!_k;Lx~rZ~y4P*MU67G=k{~0#6dU#`nF?E+g1n-#_~F zGa3U&eG#2edK|kDU=wsC>Vld_*N(%@9>2#+K+fG3Kdz{C71+igol&|N=yRF#_V)cR z8b#pq67yNYksPH*uL}V(81&+a!V9VxKOPOH(lxq|KF(rAUUTdC;qmdo(K%I(;8y=o z>+$wLE&xv+YQ>Y)7h4p`=v4YR>_UL;ZtvjF&EdTV$43vpIJkHGl3E;!2}GW9b(!dU zPlNb!3*xzw>+C2!h+PPr2Jsc4mLjp_BD*N1(`BOXJq_XyS`gVCsc7TuLF_`{G>ET) zTm0b(e|0mkXTqlijWBm*3}QDkE5z@vagm_6T+%kS}l+9@)H>Q52Xd5_=T8 z5#Vl^%f$9}`FzH(e5q_N6T5imw*NHra&oL(YBx9bkB$%b?=~%(EtXFMBLB@A$?Ntl z!X|zTc)dmO<&vA}D1A(JA#nPTzP0gN?A}WF@l^XzPkDAzov-uXs%&mpOYjK`zVa5xy$byexNR9@_)OwpZB>8p65)@2kHHrChXGhV`@%xRj8S_AnZ z@F3djvVDoTim^p{++KvK@t~IV`0+=kUJH#*|9BUHGDCGxsgx=O4`e81SjfMKOsJKU z&G45anG$}DG%G)WQo<4YpC29$!_4O9W&zNYaK0#rUP>5Icf1Wv!%KO@hRV9in#x>d zrV^L$Qd$e8_4#x>6fP?HT3~~Y$xvmWQma(?fLlO3?=jDWnOM|xG9C>B!<;yQz?>&4 z6yuSueNWHN-7ck$`Rj@sH_GN2GjKmb*`iNyB|kF*bg`yDX8Xcx=cu#|{l8G;UMP^Pz_n)e@ zFiLK6yr8yNMAx@^ zg{rEdm3qKl#a2KyA|iHywifTEwzUc}sU=q`=xpm%5l;=Nhv6dLOj@YIW?L<}kB(|g zTR|qBuCX?1u>-(?Evf}b^!0!B4a2N!^*gUKx99qke&_K-*puLw$35wM-=+Rl{XaDA z=_^M_h{;=8C?4|>@|KW{Gw{P#>+{>qU9G?F;pNIMTLaI*c zS4XqIZt((?%0-eZa#^w_+^W1=T#PR~m{ii&Hy$xoUy-hoLx(09ON;_1_)Hnq#S`%~THtB4z?0;b zCv>n4Pgw_^ayvYY7I?}!@RW7nDeJ(KtfKb>JzFg{Q0o zPgw_^vQA`6S{FR!c6iET;VF-Wr>p}{SqGl74m^zpRmru&uO%xtD*nQmKW}O-ve!`z@h75n@z%u-)J+Onn+zx;Eu%#aO z%l_|3nx=W-Pdc%7_?sq<;V}C$)$Fu^*;(c$&!0S^1RX0G_fPc+y@M{=m~b*-7~n z?eL_%G31m_A;XhwhAc4(j@o~?ZDQZUNy&=qtqLB;@(;`b8ChSkzvY;sFSnC7l4B0? zCd)Kr`{0!A!v|xr0Uz1UYTo2__{cWlBg>8Ca&W>&mQ&t@Kk$*;;UmXBeB}1Xo0QY! zO>W_2>vPxznmCiVmP7^i&#K4uI1+nDo78_(mOPrlwO+UpC&%Vbu-k|YrX zN8&;jp++7{;#RpHz=JACQo={;!IJghNRoPi$i470*BnIwJCf83H|~X*S@e=8lGF<; z$_9;q6G>`oB;l#!%bsql%~s%I7@qz_(kMKA!;;3ri1pyZdaxl$y>Q`PnD9E_!Rvqp z_rifzE)QV97?2N>xIQ>RpSJ4~myvgkBy&MtyKUQ&#)A7GFad4P2HKu2 zqCa{;TWxX0a=3FL;_C@N`?F?`a#y z9s!>21y4!x4?Nuqo|5Drc-kI#S})wGcl9=EarR!Iv$&|wHbS5c?q0_!+%-1hc*rUIw)D#I zIU+jz`$Zs5 zutKETgS3C>U4s;#f+EGIu13=SAuB092}c$1qBQYWIZ;itcv?4#i;^@z&tsmI#IE69 zP;@US)-@wQaZ;fNiux@>lK!O;py&~xXnX7iwg-yt1x4EfMcV^K+iRkY*+9`FwxgGM zLDBX=(Y>H(d!T50plEwL(S|)x^axP2f1qf4py*yuv^`L?J+j>PBm{Cm+fY#%oq=cB z;_2RHJj)i(QH!VV)a7Whw~VLlfv4>)<7s=~X?qcs!B5kUSjN-#A}YiCWjt*!qB8sg zPx}X+_Fx%L+XGMcF5_u?;A#KBGe4Oyo<(c z6+A-^JZ*0oPul}e+XGMAlMu*pbApGI`V2hBVIMqaXW&^B8Bg=NjHm4_<7s=~X?x3f z+8%h?9(ZmP8Bg0=#?$t|)Ap9}v_0^&J>v5O&*9)Xq0LEe31lZVZE@0v#2(-|z<3ti zE@NEQTN(kzj?~i_vv1~;>H8kD&dOzsXKl(JKN)x9zmja^~xDTlPTFBS6s@fTFX(Q3;;h z-5O)!(~^X^OW>Q;QOcqgaNt=M{LT%YdO<@Lz*8@CNQ&#F;=H6a>q*i-^`6rgr@F>U z0y~ezy2o;=moYZ53%w@V7#lc68#s9^IC(55xW`6Dhu%&*mQ3}$5n16gn0PFhcx+%2 zW7QPvFZF%0e*8%5m-IBOpFH9ojk3zWS>F&6t6UQkkLH>Ys|+*Z(F`+Ul~+bQnpZ}g zW|I>wzwqJ+dNiBg)w-US=W&NwAd%N)NSr&CPFC$09!SqIOlEo2v{6_wYdqobB!k literal 0 HcmV?d00001 diff --git a/RandomizerCore/Asm/z2mario/music/BattleSwing.dnm b/RandomizerCore/Asm/z2mario/music/BattleSwing.dnm new file mode 100644 index 0000000000000000000000000000000000000000..de077f4bb152ce741e14b5a7d0bd95be1ef1269c GIT binary patch literal 24815 zcmeI3ZI2wq5y$6YZy(Qli4)9201u8u5=4lBD2gINY~Y-r#ExT|_c`H{IAIJrpOw7G z=g9ZUw?QI;)UO`8cG>mV2SPzG4STaSRbACx|ElVq-tjy4etGraqodo$2k(D)c-&q4 z_|Aj7hc`x5b>qs-E7xvSrH$p=bMl!^%B`xZ<-5pMKafu+pLefc{i|F`ga7wJeRE&_ zw14HD{hO)3m9OU@-#R!sxqDbuP2Dt2+l;DdHIq@^x_$G#Yx~!4+x(c?e_pDp3-pjy z@7T5c{i3Rd{~zjsnfU3-@h9C|cMtA;2;%C$x-s|F5+4ukIHFuS_do2ee|&WR5NAgG?czLIU-yBF%hZ9lkB_K$7Jp~z z!F`VvH@GR>*S-DVgm^R;@(n^9{8*a4w_;SfzZ@Ri>8_ld$O9~-dGJT_MIJ?1hzAe$ zd4M2P55C)7J^JV29cXi5Jimz>`xFe^GsT7PcCX5Rm$;!ml&dy~!H0bwVEMRl_kk?N zuXUG}LRbtwQ|oCHfBSk5T%O);9^E-)=WMQP**{wwT-WCT7L8l?4vr6PAIMRrdC*S~ zI7#U1KX8Az?!oTb!SVYaa8vy#7otPy{U!WkY>d2#dqL!GDjJfULX4f9$Ab z7g%BtrnWu=hFoU7z5Ve=okZY|o)zy9|A zE%zTM+DCH2v+p#Q^Iu5Y+gmQroY~!7EavmsY&xAx#^arx(Wq^kMt(=-rB2!m*~JWA zQ&;{2VKr6_%QL%+IWOSR7TlVSi-I@+9_jtIuHrI;q)oGTu=iM&hUk7WzW87%+cO!Dt)ydD5k|YQJrGXrt|IvB0wk z?JPWTmya_LfW@5DGZ!yBe-5JDsOO8NzU|6Q6r9)hH-%Ei&i3?`#2{aJnV3-p$j%5E zs0Dv!i#fBNOlNcArbsRItsc3f8qX{ViNf3^B%qWzUJwk0obKu`;c`m{|BIhK_auR! zeEw9Q69q+l4DLzpY7D^j$K208tEx{wt*XgncXu+;-$sO~IDSS`)Qh(10;C~T;+2;% z57H^I)YC<|wTPUP@5Gl}G&5%^`(L4o8|8Z!@q*ZD6UaW-M*0XNfT`H%n+n`6DPojH zha?$t7|?mrmqz;07#d7XP^~2LRt7its0Mi{A6TIkg+@yOq(+2RfkNqfsfB6)6Uv9u z0B50_N>2=-RdJgC${k!Hm+#aSgqhcuuQAWK zAM|Oj(VxgUjsK$GY3=<#<*&-Wp((4c41|REoqQ->MAX0Kb6Gy#ENl3uDl|&IG^(mJ z76NHEuH3%8fAe~*=(>jfvL?X%%Pr zYbNRTe!U`DRlix0TsFU5k+itTIlQqVSygYYXwfxOXhW6x(8g``m9QSKVLdLs0BfOE zutpL~%Lc4N(y&e*SdWV@*vo{ZVI7i&bx0c4Lb*M#4oSm0BxOUm#_OPpJ+KZfhIME$ ztV43yfOSY#6+z_mb?;LBUMR^< zOHxH%D37K;7>^`fX^1j&+LSnWg|yp=eZw!tF~WS>bt83H+< zBc(e$y{URbi^ajHV8*sIBrOi=HtZ1xA!%_S)?FuYFlr*NJE3Mxv%?I`8=}SMK#qt7 zr7RBWQSgWZ;kmbogXEF(jEvb?9OV2i$(>ml3vnQljuLS&b95~ZLek@)%#XET=AKwK z#DQ4%`H}N!PYL;hiNJdD6|rtL=b06zCO!+R@4{QbT6pe5u%@3}q=9uvE>)y0c!GdX z?hESsy!Z*MLyOn%ZG1=cw~u*QPL2=!es)4zswAZR(CV*%@swE7;BS>H+X!1`hMvJ^OXS z8<(+|H!io>HLN9Kd{^_LN`^hK7M{;f>w6gsc|8@K56kO}EwCP!eqb&2-RB3^i^5|q z4@s}@WqzL5-jBQ%3qE%tv@wamdeU}Y!x_F%l1uE=h=+-c8??llQ+(f^yM~~x*V51J zleJd;Q6Chw9^IY02s`GF^SZT|G!^|}R~lTy>D)AO}DJz>3`%I#CG*Is-o)+Cz5 z;neJdwNQX8{^cSSNG%xFGAE^$4M!a9DDdQ_P}*g zxhd8`UMSZI@*!!+&t1|oSt!>D@{+ASYO1zS&g+C?wsMgM@;Td!LWKpZ+B;>oKNH@# z%=YI-`N+uY=w~%N`q_Fdlv^hbLegq@_{H{Z;TK!4L(*!vc-Kcw9E7iJy`Fx>eH(GW z=LPxD@LI+}dX3@NYjKb$=}}kl3`5LWpey=W97I2hgFx3RKuB5~1iBUnfv#2bko28e z^Q0P=|n6+N`r>K$5a^#)n;xswk~R@14iHr$PM-?CgN zyQvjjD4o4k^pLc?77E3ZpX|ochvaqkWB8e8RP@kdcnanA$m^Je;Te*a*CA z@;doGTg&UvVii3kEw6?0`GIFh8lFP={HW+@4?MG<K4fJV%I82{hi5DfLef_DkhHuG zZ^vHdGO8qAFX}aPlGo{%t#RNnqAUAO?C?0|s>|Fp%Zp!rB|NFcp5?@d%+~$#>RLY{ zCjIjIT0eFK@+9uDMf}8UOZ>QBv>v#m#ZO3<`0+WQgRq9|cT7N9#ywNZ9Riq>XUIro$tNUz$G*023rVZ*BI(ggeUBB)pSFumvOMiY zUdv5s$Pj~r|n`-*K4PIy)L{9qEmKbIwQ+#p*$zRGkM^dJa%fyBd?Q3 zCMQpGx6F@h&H0g^5;6Y1l;@{LQ^`-=znA*%ImmiVKfT16ntIpjy9Z>!bJ|SZ_i&cg zWtT-=+y2mb;2DyJXGltgQAyKccut$>2cGh4uIuCyT5SInT5P>ed*B(8wqA#%t=DZE z_SlD~J@8C>-HH~=&(PveU}b*bnfAalByGJ;d*GS&z%%V#TG3+ruh3%ab0(8OnaK2Wq#n9^8?Qq^)@`y9(abN3qpk!%g?&t0-iClZFtV=m>+n~ zN2Vbnoj_Re^Pm1+3@gWOozmx;^W;1J;qPw)ai^gr!)6V+SEEBIvuC zyHWAB)#7=HWw6frw~6(*)C#S|r8aG1o%S|!H|>FE+5^u(&`+zIRXoSO(v@c!$6C56 zp6q>JfRNUfIc>|`sMD6awgcL>wzg%ykIvoLM{Ud9uxGg&_BL}j?QP55*za{~^CNd- zwqA?dndcdJCT|nZbpJg3@x^{PD%}BZOuAj`Dy6dX>}rR zD{UYz-SqcGt{s1*)m6*FDGhY1wViNX>057d7j)&7O^*^f$Sa;cQg*_1b>4Z3Znft; zDs$@OW?h|g9_S{Il~(LI4|LNW=%zh=vMKfy-O`WBoH{%Cp7uaDc^8!z_B30|{6IJD z>CUOx1Kk`a*_t{pDKF+nwkA(k+A>bf)?!bywfIwaPQ{+0Tl#@#>f}4CY%QNoKD3<@Ux>TUJ*$L=#d1+Ibu-P$SJ z2?u36;jH1*;>6Qx@yK;rEw)v{-`}Z%x4jm#6F#pRA61KE-K7?%{jKQoqkyz;YBf#k zkIvRu-Auwy!5k}{-+zWTfwUtDc9Z{MnLXa4WAltcLO-WQNB%&FS{-1@kfC*8Qv0Nf~ zlKjkDWn?2p%Tg)CknJ=mb z&mJEi%y)WOwzK(g^TBSGq%UoQ(n_kzvRt~(&0f=|(&zs6y$^Lrg8gr)^z|tpY;E4% zdUz7Ql==q6o!P{svf^_`a2&{_I?b*>$wR3!MGzXgj!nKw*_YN=9 zHIk2cZ+3&6NgdL$Ya;-=JB!)T$E@RKH_nG9*JV~a0VeJTvt@PX;MvjqF(eIelBcQL zhQ2-ct6(M10Qli2^F!uq;L~9$JbI76_}ZwYtO8Smuex)*Bp;0sje-Eb zP^4G(G*-I5%x90Q&E-;euoO-3J5qEj!baQ?Y$HGt2EqH)z5Rd8A5%8M=Kf6>+X8gf zwS@3~^^X3#MMW^wF@!?Lu#EuQ$IijAHsg1zTk9$~2j9xMuF3e@=G|GjdwaP5c+Qox z(Un|3TRL=Y1lTlo4`++HuLF6OX@d0=1fC?c`FGaW&l0R2%odM6rZPC{Kz4?79NP%6 z2s#sWL4835=i#*D_hJ)}eK+yridt8JDF-ozw2eSFm)URko;|IUf!~$d>?Lf;AsxLo z0?c6O#S?|^sbGFQ8my(wyURZIVr5>nyF6Yl=ZlLfn&5@*q1KD7fm{HdJk*LOtNapb zip=O*x;$(nz;d@e|9iE6c(hy`Kb;>gFRA7XM4ocBh3Ks3A-f}##}CU%+DPJp{%E)&!3^3{f6eko6viEZQ1 z9{*LavU7alQoGujEtdPUgDTNXEMEm8|4Sd4*Xb?7Dt-(2L8AC(&dqd4my>M-&TrDY zTYvp%Yx~aDZq1LU+WUISvsITlT>V@!ovyF1txYE5@#^YmG#m~F{r<{IuUD2uq2E$@ zu@f0_?s$Z);DuUCKO(HJO~$Ldgoli&8un9w<{@w|>dQR6NnC_)RzGgfBGjN)Dti3* zqfl4kjMnz?E&{U3sl7syOEMnFAbCV+ei0c`%7@cvFIzGa?dt1W`3V$>hPeIB!^4#b zGo4N|KqJxkEF*i72qJU55>3UW#$#QwCYeaal2u7uzU!n(oU}F`4OYZO(p(FyV;HVT zdXiF7G#lJ0;>{ZKT7-#CjfR8%N-%6DP7oOLLH*dsCcFnmX zQif1t{*1<}^m;HHt&%tKHAGyGH+xhQ5!cp9iDaC_OBao?KPDM4#<{~u{2H!eH2x<) z+<1dT2s?kW&&YywdY^GWDNZ5>u-3P_&!1=6r=MooU@(~s2J!odj4_VSkr?$9Z8&D6 zDaIK$Z}9bu^vN0P@q|%T<7UoDC%<%vnK_Dc|D#gED7|X(h0{)phU~KtrjHEGz+_zL zI|M*AMO=#P&?MtLz99OEFT(UgXd1?tps|wZ%?v8|M1-O!8CJ@Hy@;iNVq`?D0;v@5 zrlwNCU{b1^QZSsUTx9Xoka7gh;?1N}MMg6fbMB*~7++g3m>6B6Z*02@cgL3buVyor05;FQ{Xg?1hy{C{0!- zimj{`Wmw67)hX4<_I}i{6G@W?tA;(;L>!NO!tvMzY-O=fJcZ!C%<(E|Zr?R)iU$LG z{79PP{n(nUZzt;wX>Q+5*D3NXv}oVE*5vUsTh-ZLNOQbjS(EiQ$@)8KZr>kVM`vpx zzp7dv2RnF5Q-m0=#Ir~|rO6r#PgrBmNIZLqr&|P16N9I$YA-w;51x((Pg~(Bb~R7O zt5oOq!PD)7r#;~5c<^*Qc-jh2j|Ds(51wuxJl#Hc+5?`B2T#X?r>*dG`{3z#QevOt zSn)@qu~z!QQ<`AHGmknU$VQ=&pVDN4r>*cDhL!wOok%cb>a|XXr!;xM(;o12Ja{@D zJZ*)i;?+DIuTq`c2T!-}x-@&h)A8Wxc<{6po^BsJ9S@#vA3WVYc-jM=jt5W2gQuhM@oJuqSE*dG zJa{^ul*mMm6@MiCVyHflpX=HC(%_kwuSk)ILIJc$?i8S!o<4|v)m@-x~8 zPsf9&t&yJ*&pe42`5Ezans*)@D+-d{m+%}H_`%cGvpmP)0nc8X7x`HBWdPIoq1l7_WPjLWU5*HF;SBe z*J2#gO>#ESWq-{4tWX)%wptJ26j`AVxo z!#Cz^)RANMCCBU=b2fa*G5g+1zA-<;m;T!~`XApL^D}(uzkQ`d57{+n7ves+hqR5k z=vd4}w)n7t4Yu!lVDU%NW@ANzUoFWVIDDR$hVitSCb$Y z+j2setZ8NWcJ_Zm3Hy$EQRIv|*&$8O1Kvw@rn8}R{<-SPIFGp_P0x9Lnd&@?Hk9nY lR$VX7gPk;C`% literal 0 HcmV?d00001 diff --git a/RandomizerCore/Asm/z2mario/music/Cave.ftm b/RandomizerCore/Asm/z2mario/music/Cave.ftm new file mode 100644 index 0000000000000000000000000000000000000000..328cac02e578418f7e7331d135b69acde996c233 GIT binary patch literal 13010 zcmeI2TXS4j5ry}eGn$JOk6^gpCou$b5BFOj61H5VK*Gq%JFY|pDkpZ8oD?rq@dJ40 zr&0V6UUhKf|O8+HjJmI&ze4cTfO%j<>sBYjt-9Ryngrac(Hr`jd$KW z+!~g0YkPlt_jc*lNYWShwAQIgX(S68vr`|959gZWChUn0vdk@||e&?;jdv6cO8!2tZ4Na`Jy-ecj3^0H8 zkKQ;WUCG@+?LZ}SRrW8hPL#6W9KC*bad3P@8ng2T^qkgZ`kvA~>==>0z57QG4uiGv zPph5gGJ(&)J+gKE_|Ahru?Kta$gy|Xd{1e<(|gCbOQOy?^dCFOQ>=W*0aIlu7|aI3 zG|UZTt+bIq&BV+6t;)dZCM2WDWF^tR4_2ADO!4U?-ro8BYdd?_cl`5<@%T@5t)_#R=4zfe?ExTDq!YK{&-HkeoNa;Ec%N1d{6(yo_>KGZ98u=&b;rJrI!y1Z{EC^a`UVfvj5*hZby-yA0SuXe$-(~`AHA+vmWN<9_E)ly~J3MD1BB3oUu-$ zbE?y*SEo_0PNQC(5|edmdUY~}I$2MhtY08saGmTwb&~&Go$Ld3vj5b{80usUbs7yz zv#!DM3(46-rBE};Ve^%gp-8vjQT87_BA&hc>^vKuQ~ic5R85|34DwL|55$JeT5t}Y zCC2qj3_Ke~?MtD48vB8#e#2^KA$fYi^_$lFQa}0G)oAGyk{rCDPLg8}s@#g2gqG(ROV%=<(7hLY8pl4Ab5C-| zr*V%@iK*v{9-oYXPsYF}+gV6%)Yo&p=l;AEA)TBfN;x?@l)`yo+`@xW;Dmbo6c1|e zcl$?xK?2v({GD*NLN8hI%;MqdqLF%c6FoB0m?J)J6frSY@2C%QU!p}Bg zLeKqdX9Ky~;asPg6Zk8M2|Q=KQMI#~n9yq&bawdVB24JXpL#9qd@M1cXYSdbPb4Pv z%s~6|slpO3`T>dY6Wl#}z+EU3UgytedWQ9tv+{RF9+nn#RS4SzC-A}c;e+cPd}vPiVEgbvnDBqWcWa*EgYCly?eHiT!Rm;JA827>Yf;L1U?88_k|C>FMKe*?v;Uu z558~RH$x8}TzB|jJMh7F;Dh7k_l51i2m22n9IwRS0{yvh7HqQc!~28n!WZ`kSo^{L;C&oExIfq*jE4_CM^R|w;e+wqPfrs&J{S)l z+B$dPgD_DK_6OtPgD|0o55~g>U2$9e`8`J#_Kx1{L! z!S^ob`m8z6x$gd3p6lM<=wtUEeeClOeeC^>KJK6Ueg2`3&0G3dn5Zv(Z2R=FdAmH< zZGZWk^Q5_MJbmoAmgl<9C(L!nwLI5-K4GqVpIM&kw!>VvKg)C7c9zdMo-g`p%ynim zxng;)z!S#Lw~M2zu53RV6NK^bKUu6aN7=Z-FAY1b)Ip~v7MOfRnJ_v9p<|4 z8*{zZHRgK7GuMrexn9o?G1n{p)LgIoICI_pbI!4y<+*PE;e-8Ip6j;5T(=$Oy7A0) z+gV5+o$Kjxe zCHArW2GO@7MS3{NZYIIY0k{093^UZoTBDtt`KFfb+df|Tg?{4Qsh{H}yTgv4_? zLOuIi5n*0EON4qrbSlEM@}vm!>br|Dzv~Ez@^pmytHz25^Xge5+z5LM-77Oq4fPiZ z-Q!6S=GAu>q5eoJ%dD6<>(oX39!u+|LKUv$uT zFpxw;ro{A5i8+_JMMz_Tm}IG=GZObwC$W>K4v!KiA)YGJ)jdCCi^==m`$2F1Klnjy z+&$FgBJ;s+l6wsm8VpJ^Z}J!JmeN73O8gy-6O<&VtA&S%y=+~mm#ex5u5p%P{qNa& c+?{URy1u)(y}f(uxA|Yr(67A8&fbmx0$}n|!~g&Q literal 0 HcmV?d00001 diff --git a/RandomizerCore/Asm/z2mario/music/Credits.ftm b/RandomizerCore/Asm/z2mario/music/Credits.ftm new file mode 100644 index 0000000000000000000000000000000000000000..d2c5090004819b059b4f6ecde3969fdafaad6d32 GIT binary patch literal 13864 zcmeI2*>hZ15ysD5W;Ba58i%DIY&XUPvnItmRT8#bp@F1c{DxUXZ`x?K0SAQ=kVhP2PcOgJv=(8c8~A>{?XA^nbO_u zd)vGFscmCP-{8}zRi%_k#;~qerPt5*?tGxPwqgHO`{}b2-rL!}y>qXdzmWJvKJFd; zv3hd+_}3D%l&2z<+T;Ggy$^SH_6~eH_Wy@#DJ|%)mT^Ef&L7@i4F6t%z?u+x|LE|3 z_3lpqE6_81MsPsyRS3YfpFKK!{4j5@%knJ92Zpa2Iaud{Z2$4$$x*d;{NTwEG4I%| z0UQuqMWAVm;K9iQ<@_4ILYa~_FmOcx&IHnXhrg?CJvx4JbU*Hm_)}&BfGePL`q~hH zzj*M`Lx-_Q$Yty+WTeZQHjr=ylI<13P3#0X$_DIKpl1Go-K~?uC%>6A*r}Wyu=}5} zi_(Q##JnprE|DQ-SJr0Gjl5SW<#CA^zOggkkn&d5EkG9VMd3#sXyn#1?vH_gEBVJ& zad7-G^y900LqF65dMnW*;elSTj}pfCt))p6&O^nr;{G7}Q8s_@+oQ+(JHPyJXYbZd z^Q4IYepA)+7xdn=$qTi`=3--gJ`=dX&{UwDu5p70>QgL~-Ou!jYluHNy;2^d(DQL~ zbCVPsudt8To3SHze^%DtfKr;FnUrNwG`Km;{@tbF<_#$rZ!d^sg-JlTCSF<(Yc*|vT(Yqr=OHMXB5!tw2b9n zJpa$LfB);#PyYFjzd!xkU;pCxPU%`K_43?NKWeBkY3GiKq`oZgcl2DcrHl532Y#v- zQHjg{q}RK8MFX=0cnK5}M^50H062kk0`lzc_QAoH(P``( zk~}kyIc@#2b+WQ{9_zl@TE8wy`_*dSY^AETUbjwTZ%FdGZ@1Q)*2(&PNuK$Kt@X#P zPC_(J%gVH?cm6Vv{ejLVMlBZ^NC=bVBL9)kyId4tB2Fw9kRzJFYdK**^f@7&y1
zPv&mUKMg;~Wv6n2bhZDd%JlYsUhp6Z|BnmBQTTsa@FnXa|J#cH=S8M{;D6~Pv9{QB z#{bgQ{$GoJ75zfO|7FQ(@c$^J+W*V>eY{>sHxI8bBDcYR_j^Z@brPa!;Y1FijnB!6 z($(i57Y+ZXMZ^ERZ1_JeU&;Tl2LEHf!T*przjIN?|F20mQj~gUNQ;a}u&$Ei%=%WV z0~7ZPOkymUxZigrDR*kV@3lH`S_@s5F~LbEQ$KL>SP9X%g)ci#;6$Z;P9n4Xe|bXQ zdmVV9TmB4BlozGz^MqUEWu7QaM^nH=Q|u_cC{@TKL^!#2TBN$%qc^XHRkMPEgWhHy zy`|F|F{-&_#S6AN`(WkvQ3T0E_OVr4LnkY@k7w>zB%YbI=P|a9K8?W3UX7TuR7vv8 zuu3}hyCF&UtJduz?WT3I5?{)QvUa#}`*<}8u9x-RR3umR{8S|CZAGH<*AB0gml6ePFN?1;g^U5C#-`LnpLJgC#2I@IAK5ZIU${_tnIN~ zMurm}3nyY%sgl%j!n?X5of*^Tgc$=TRHdEagmhY$#`zIn=krsMd{r-+vFzsrW7+q= zbZ7iOI>-Og310l~nfw0t%wX@aegDhDN-Ox^aa~F3{4bqmzR~LNzjWHw%~ps1&AToq zL~UN4pG!GobLZrdOZrGir#0#29T5o|u}dPsJ0cRiW<)|*$&ThaNSNz^L|9#$F*8V* z>m3r*Cd#auf}J6`HnFaf&X~N__PgHp10DAR9rvR)v94d6Soac6v2MRMA!4{t2F8E{ z40s(FqevA4`;2jX_8B8?t6!fn#1O5iQuIGt)CX-5l8CQb8XXNi$P)LotQak?g=QRm zn8n{HA`Xi}S9ITLb;PS@CYyO(Zg{UtHnUYiUa)+M8Pths}g?B%g=$5x*oE9duExMQn?h&xW!R2EnH{PV!BJ3kr7h&4#yS-e5J z%1B(q1oCK?Mc~clh=lWQ`7RP{Juecdz|D1H!j;d>R!3!JT|Z8oC%CCR7RI<%3dTg8 zLABM@Puxy4vM?==r}^AzDSjQWeoNiRHPS6Tg1;b4exQ~4nvw?bILdN41Aj=Gxt@}w zG()D5q<(UbYK&5EjF(Ol-)pRc$JS9RTZeC@)3lW(&CHD?ou&mrNtzjqJOX4q3wVt3 z4XqJ3cdRQ_+{~P``O((h)vYEFY!KJOj@|K<#r&m%;mG4J>lQP%SV zu_6aWb7bHdpT?}3$;=&Wog)LCC|v6t8R)og-OrJMj$76`M+Q3XR_h!O=)7Oz%lk#= z{i36>Gxk(})C@Ci;&Sho+wRnUC0_3Rk_|gfe^&VRGNmvHK1AN@`0%!NVvdk0Ep^O@ wOc~jjOlhlxxE;v$z89KjL+@bHjgV)z@7~(2_P2NMzVH8HL0?{JXYclZ0mirL{Qv*} literal 0 HcmV?d00001 diff --git a/RandomizerCore/Asm/z2mario/music/FinalBoss.dnm b/RandomizerCore/Asm/z2mario/music/FinalBoss.dnm new file mode 100644 index 0000000000000000000000000000000000000000..5cdcfef2e4ed8835003ce26c58d53341f9e84e06 GIT binary patch literal 16751 zcmeI3`)*xF6^Hlb_}uN&21wJAHk%NkQiUkA2qDBJH-|f_J{CL) zPr>W(E|r2>e&71-J+|Yrm4Fbm*(1s?Bl9lJQmd z(?Vb*LN`~RSGV@(2Tz#s;FlMWQ$q34@oOV67NuMB!^7&q(f;8AYz7F|TH302cv-Fy zd@Osj8{{nNkd9p&0odJM%@03g8#lX2J~X*5vf2r-a6g`}tK0j}4i}FhX@Ju_E!{Ts z?ap5ZD{%(EPd;B9uwDb74m0UlF1#JSHUi-L-O+MAUsd-84ZggWPr!$?Rqm`T-`_7E zJ*jq{Ee{vKY~pVUJfs8IMgU|1aBsC_$Mf#DnRocwsHLa^Q-iO%eY7SXjS!850Kb%{ z*R~it-5(b7$JNdCS~svHP4GKXbSuJ%xFgs`fFKNl_o}d|Lp21gx;&XA5{8v!;! zN1`sM&ne+JoOS$OZUJ)MP5ii`)>UANL5v}7GtjMN&bNEdo>q#$?@DdXC7hB&I(lsc zSi#VXCko$F!u)tNSW8>&F8VkZEApz{_0f90Sn&$a1kZI3wO(!yo8V(18et%=5*DK4S(7RMV z>_kSCos6&*d{ArYhlj1r>151Dc*umTVLv%&9s>6wzs%Ez#6{H2>c{O_xEl0Id5@Pr za`j%6Xzd^0A|PXm_6kWZ$#@`xGsv`r7ww``qm!Wwc)w+_)jLuW5n% z5#$^;;i~)D%m60F6lAVke(yYql#!1oTk*v!nK-x<-*74wU`Mm~EU`&myBb%UP1zA4 zgR2RDMw2n49t=lg;wGBG<5qmxqnz-#xkX4M<1{|HXpH>{!GJN!4yW-pT*>JCkAL{? zTLePb`ICJ~6r|#P=KVN1jTpdM-|9Ypo@HNrk!6FybUGNs`w&e@2!c3El z64$QMJTrYhOm5OXeK7(LfIEycnP9&Mll+3x0j$|}jFqs&gVr-;h2Vf5DQ8^&d;ahcN zh8fq3$7Y;Oev^LHV^g2Vc@=(5y;p7Te_8&n{vS;``>G=>h->;~^CF`Ds^1&>t%Xx< z9?P$!WZ7f6Z0Nt^?Coto+-c6@aq|3Ib6TpaR*j4DqMXO-EicLmt@Ly9qRp_f9D8A9 z$=J%iQHGV3AWe0Nds{gt6=7xnkR~gq6IEkS|v^Gsw0=J$;%aK zqv+p{qz%IQzBO4tNY<;;`e6-z(Z=;;{ZQIQSbt_sb$_0$zmVp3e`!tDUnT2rk|rgD zNUN;YOyFrI@Ej(d!)Ozp(&QDMw!+g^c-jh2Tj6OdJZ*)it?;xJp0>i%R(M*gqHfjfR(RSAJnaRZw!+g^c-jh2Tj6OdJZ*)it?;xJp0>i%R(M*I5@#%q6@MiC;-YZD zQ<`AHGmkvta}*jprO5b6f?^{#<;OV;Xv=yFiqmECt0Z&`uX)8Qk7oN7l)7CmZRTrKv zCnX}0BixXFF%$+;TiFe1m9$ah(R1fX@eZrwzf+Idf;uvMp*=VjWucbgI1H_da^xNN z0Rnk>PI(_C4HnLWMP80%g~hOVQyMJX8Y~8p2McRbVoch^POMc}xVNxita*tqV#}li zw5AekwWJ2I)erxSGLJDRyRJcOnIo}ff5evk5nHwrTY30{iTwqW0uq(-JTJtSYe1(L z{Uf$)CAMshwHp42tx@!k*wUSn{9R8qAnO|Nv_E*-A3SY^XD|H0)BfN&jQ&XpaxkNZ zi0Pl^StOpep5|F3o}NQ^Iy&I#8mD>MA3SY6&C~wiX@Bsvzbe%@&C~wiX@94A+8;da z51#f1PuDoj)BfOTD?A;uUaQ0pJniq-NrSBG{x)e+;);qRF^A+?DTL7REHVCZ#%&An zlw~)BpYcmms55?Pnxslc*QMvKOW$0VzPT=ab6tAmx*S z)<~Z91c;fm#{5}h?ADmIJe#Rp>@T6k{t_DVr$;oh#{5MdjGPA}*M*TaDZvO%>9It3 zV1<}GbEwkw6O=qVFaNPegnwe%{+Ux-nbSPGq;kxuTdI=PV`U7)uj_i$cxFFvU5}q~ z*HhhpJyz;^th9SPR#lA=G9G$}xR)Lw{@FL(4+Uv$jeRr5P>)~b^ix~EdE@7Po>#2M Zw{iT;~R19 literal 0 HcmV?d00001 diff --git a/RandomizerCore/Asm/z2mario/music/GreatPalace.dnm b/RandomizerCore/Asm/z2mario/music/GreatPalace.dnm new file mode 100644 index 0000000000000000000000000000000000000000..0eafb1ea9a9c904ea4d9300991568b4251a8849a GIT binary patch literal 23836 zcmeI3TXP-75y$s(?&nA{;%11!3vt3FKm_DUNP;l}T#1Ean>QY!!m2`rF_bJ<@{p>0 z0iN@eua)mY6)DL7*FC-GXe2MVxTRpnmdSr(KgQiwLSXg zI&W^De{K89(tfS(X%62w*xNm9F7MvheRogpmSt7c#h|>nbM@Mlw=Zq)T=NJC?mtf# z#p7I~R;~t#;Pd`}2Le+ebpGI@=G7a!H{WN@4L@ORtrSCg{JIdB38zk2mqM@cJt?c3J zLak*LFa>fg0Vcfq7#x4W>*|iqJi_P=;w}rqk)TcELdJMY|VEedyA7n-eg z9XJOcE~#q@es^8npXKiD%Kr5|DrbkTrhfL;L)V1>o5t0fy9aw-2l6n}5ll}Ic#_a{ zeSh|zqX;&ab`Rct524|x57=p~$FU0mlAtqD1@)#5oQE4deqU|@vhTL|QBkW3Y&pnD zYh5$YuVwbzYaf2lh=KR18+!>`vb7$)E(BP?pl44M?$?3i$D_g2x~}fo$6hSvHCGRB zA0F-o|bmOWX0utkv-om$VsE(Az-J9~d?_HW)gJh=VA-pxb4 zPj)IM5P8bgWuiZO9K?rO5FaS1v#s?Yb|G*a#D|4iVq!^=U6j)4GSQzs4&skm5Xp`l zC~@{6b|G*a#53S#Uwcw$bD%}tWuiYzfs?+nyL#!>7kQ=15v)sY_^kB^b|DZ2&dMX3 zM+KXqsE@?No@w0(a5qeu*xoLm4GhcEWm_h8%|o~SAHrF7j;z$yyM_mc`@1)q7R?sR z9|9s@B}UeD`xaqi-vvC@qWDnB&9t?ilU)cL-=wc@|K;uNoeSGn6Mj6^-q%x}?R3n? z+0WHBHrChI))tHTd^Vd-CzJ7bG#U;EgSxINeV59MowO;s^C{=5vPfSN79+E;zP6ap zcnOa-M>H9=2I3*`z~pt=zDiu#wMBZ|?xx0rTGs8$ADMd8G&=p`T?E<;)j_4E*L}-^ z2QswMEW|e=6OPKshWX2uOf$bm8kMg=X%>_F9}f?QW@ck!qX1}{IbRg8mu5zE+TMvq za5}D7S6fqCsLj=8YF55WX<tlA9SDU`v~oL|%VjHt(x=?vc3Fj!o- zH+v*8i)-spqCL5=moDPuXbu@TX}XhzeH*SrbN;CxKk^_%u=D&XK7$3V`iOa79xjXn zoK3g7pMF{tpL|jj};R0W40grLp{I zxW-9apq5GWW(ER&%tBSwOe@C*du6GBLt}(xf%eGWO>K`VCX=J&O2u@x$5mlZ4LLT$ zg}s@ya>#79hb8yXc4)&^OeQD&h}bx?48R*nMWmOG^WmOLzCQbo4gsN_R6WX zfBGV)uLNE}aP@V1v-a)S9*GONjx*es zq_gUWyis2X%Hn!l{lMXz_YH@iF6D*eXt9(}FXh!U>Ki`qY}U^$<;9lwu~WXVlvlq{ ze=@8F;P5N;>tSV({YL%8!27>dzfAI%R^%_MKS?@&P+uHzxYegIx7eZC+`O}y{uIaN z!$2DdJB{r?*?eo4k-o4tOug;~%gMB%?SuU}jaSHe#(S6mKPLLaU;zYE*b!oK}wTfzaZgg#se z{YJgOGjWBuD!3AO{zU!Ihc{v8i4{2<3O{|Bx}WeV^nbRZ54X~|!7alRZUvstujs?G zu=C=I9L`0&zg&^SztI2niX0AwpKvkaf{UU5yA?US4Ew)dk;Bo@|Kp0>y86YU55A@~ zbd0an9r!xA17DRvPvmRp=X{N|oAZ_UPvUFX$@z+(#Fg_k^mD$3pE+McKj&-M&-ohq zIbQ?MoUegr&eyP$^ELd;`5O8;UjxsS3tPSho;hE`PR`eeH|J~U=X?!6bH0Xt&eyP? z^ELEO;wzg`&sSK!%&&o0&sSgn|4)1+Aq8Jktrq!JTe8=Ds=eBJpya`eT3!!G$K>3{ zsfKIwt8%-^sRwke$;q$OdHS%YDQVoO2N>7t0qjJ64Lj5WVTb%0cBlvNQy-RJ!%ymg zX_Av)!_U*|ho9tE>6ky%17RP&hJ9+suupys`_uzrpZps38|!;Lo!5@oAujT3*r6VX zbwYj(JJbW=C;3%6<|p+)_(^^ZKihiX`)Ixyzn*rq#hV2 zJjt)&Cmaet$*(gRL8`)1AHLGx#bUlapVkiBIrVIwsHa zt8`4x{Z<cCwvV*^ZXiql3xSQoUhmkzJ{GVzlI(1Ys8!9*YFd* zhM#aK{DiMzKhLjWAHIhDJims0_!{=rwFcMNi)fJ#Dm(ILG!Ov36KBrXxR1kzbXvZ~ zdV;U9kLP@qPRrNG2kCz6>7ERw^ro77rUH!#Ruh)I^`j|mVHxpbo;Sc zq&#G66{!KMPhzLd>N#n|9$4!d(?c*xa&kuK2z=zG1J;rwDjZ+8bLxE#Fd)Hfcbek1v8;QEiL zzesX08V>wh9P(1g4X>n6{F0jw)M&C}K7fU;H6IL%qyrX`n~u7M1wrB`ZJe$FpGnfg zh_11DVEu-i_(PxTB{v=TCb^A~Vm3dBS#r~1y#yAG`r#|_kJ7coFCEh-{*V)Y=o7!> zrbGM@8}SSGHvaIR_+viQwfF%K-)N)i%!C-hWK=G>AbHD$kYjwhvUVhIxgfd4&#&+U zj3Q=mk=*oIJ0Zu9LGqjY5;31s-`9Cju#c&KiggJ_`Vyez%gt1GnV;Yl`l;?RjKB{2 z#JHs&**70pUy_^82~)$4T8QW3o3aXzG?v*R2gUPpFqPb{B?pCEUBj0PVy$UB8+(;s zoR2seH2i>H=f>3zPWQc9Uz?V{rLQ^OEXkYX zNA)Y#e(Kk$E;Ic`QhbI(5ze*7kI>`det10zCSM!_SHR_E8OxGj^Rl)kqid;SzgERvl2^o zzdqoUo)xGEi~KC{PE6LQH!{l}A7c<*Sx?m?wuznnxq5!>mdUV8gzf=3zpOVU+PS}RB6 z^0UO{V;qgk#~>~r<7iwy264F+b!@&^qGTc3w=rKs!_Ilg{6C>_mow)v5aHDHjz%~& z6p3(QXPK{I4Vz)4_k8BCobC-z`Q0}22NPoszl~VG8aIjS=o~KB+^D`+XKBPmbrjk3 z+vVnHfFG7ijMsPKW@UQJoO^yWF6ZV;I=jkJ;-?WIbx=h3&CDD-?st`6Y@5{h{uD@l z6dQ`2+^;u()g!C8j4U`SVDxflBX1P;JY>16HInEGB_URtUu&~u_LwD?F(W>Puw~U=^=&Q+*XEtac&ze}Sk}qD5mfLJaH@Z@s P5fi%^u=Hbl=e7R<%jTEX literal 0 HcmV?d00001 diff --git a/RandomizerCore/Asm/z2mario/music/Indoors.dnm b/RandomizerCore/Asm/z2mario/music/Indoors.dnm new file mode 100644 index 0000000000000000000000000000000000000000..3bd519e92c6f499aeb633713559df26edd0619b2 GIT binary patch literal 14124 zcmeI3|85*L5XbG^`@Q$01ZdI%?NW*aRVf8tfHb|(2yzWc`ah@Uq@J24)!hYn65fn= zsZ`W5pYhrk8f3Mg3W<2yJ$q(s@A&KS&vot|eX%)yGTonzA3mPU%I)KWlf%hQ&6u6_ z-SzD~6R07vkKpP-WQ>)#q%{}hDdpK3ZQhq45c;12_x2+9hU<5SyUX%(iM|WFb#!oi zJey0*n#|;4?AB;+fA_)maJ26#N43)$24L+~>Fk08(%gw|(I%R7hTqsKJ2 z;%76E6GogP;TJ=oBPiV&&*$aN$>DqgYz{zhttgf~!Ao~l=cCU%?3$KNt(35fApmw8 zv+?{9W4Obvuq}sN@~k)lbliL6MY(Z!Jf9qZqyw;PL)VQ#A20koXz?=uy#I7^M1NKI zBp68CSb;ae7efH}zB`#N#EW|ATL2#v%bc`y-|wdnAD5%!>3jm1-v7FQ2PFVw z2mqM^xIddR;JXGT6S-{(}M{sX9r!*`q@!}E`|V;#@^9*HgW4f z&M_T=)d>P839CHLBj9#Iw13I zz>gI*=l|XZQ7Xk+AoXSD+x_DwrFh_b5_KM!Tc%{CgkB5*dNAneiNgEBIQ%#otSFW{ zc^`AJcwV`;I9V(vvx~wy1Xt3b*6WRdEC82>T6(g&8c?K1SCsrPh5*CeX!1)rJ(@3O zCr>6vi#3rrSFAwflq*(5(w+tJQULLyWu2{*K#U=97R1YfTIGqBMYdsbjunx#XF>cR zfXL_wBLimwF^0fd5I+Pr{p`s?8%8>Htcav#fm405+uOeL4Y#Qrg1KeGr<4$kA>bD{ zr;co{2yBX?1eGTyPm3eKZkT0a*e+l77}_-(mWi=;h?jpJw9Fi@EVav>@oX_2AC>{l zfaQ5WGE9?bv041XgBMEi;!On^ij0xLN%dWDDxBCDa8F4b3u#IO0`?fMqM%V9Y+x; zI8lKd9@X0S?EBg6Lb<*_&)B#@gTHwL`w?gboxoMu>01C5by8%mU%PS~@HURdr%9UZg%aII6uw~$3w>7vfJzr}oO*|JGScw^ttWXQx*vpTD7Ay?NFp~g5b$UT zS(ekR@CtjTLjkOMgpLA%)4QpG%V5gVPa!HgF}11V|+KUxlc zKbLlMu~$cemXxrfR?6F@4p`YY--?M?X=|dj*p^F7W>kqaYO7LF-S?r@zuakPUM#9> z4z);gV28{5Wu=us2&{SN>5!zUEXrzYQmRfb5_5xBkrM+;FmhIQMYf_=N^L4Dsg1L; zqcIRf>;Aro+PL<27JVvO>@@~^olJ34nTXUv`U!7_9B!2zf|g{BU_@K}Gh}cx-&RXx z3hJrIXpK+dfgNuT1rUIAZj-Q;Q*oruQ@}%3MT$MAD_=XX&A9g_d=&y&@cX|w?JFhGiZ$ezy1Iwvdh&&#<~T~1PC!gu7 zYux{r?&|9!es%rY$Jg)7k53-nm#%f)R6VJC@ohrn1keR%Zb zNqhV0!zahU765{4Y3qh>;U!%id}MuKH>yc$w=H(-5CFT6PL7`ZhAJM|P3oOMZW~$a z5g>7Yb#&T(^ziYM?OoFMA0K@FMSJVlsYd?B=N<0RHP+SP$=aTfD?M9{h^!Vt0d+qd8`&TXl!S9LC zR)mRoi{Ls02tp%xqy6N;za8J_Iv~vbn_yfgV0(A3gYZWCH~Q}b7{*ZFZd`0JT!#SD z$L)ttH5vbH`@z00A_jj{v#zQ5yKeOMF1xoo5AGkcat`Qvte>N8p<9Ollg8agM<>Uz z4&)>=5G*GMoFuFpzrA~L5yAH6(aGn(foUA|MRdAtaa@N0MKB^!3+h8%L>wM$@q4}m zWZv!YV@0i1U>AejbX(_vZ7wt4-h2FIs|b8eWX>f_$!=Tp)*(O!gPufgmaWq)k zy3yO{V=h+YwRcaSo}M0`yspbY@Nj#m^?Wsu1>o#Z%O|Tp=ujl1OWVfbIs_d72jc5B>uk3zi0cqI3*wK2T8YG(MRrxI z&pHv?yJtcCiw;DpBNr;1TM*YFa2CYB1UJ9-WTDN4l6swp?Ohf)<(1vtn;-rizo!ZW zdo>$Aw=II}5Xc42+>yVc4&53eisng#76SE`xRm9cLA3=6n|Lz$DF#}&M#{bI6Fz-z5Y+X zzJBYY>vsW9jZ^IhI_0_UtN8QQAJq;H_V@Sprqjt}Jl@^i*%^(7!@;26@AaBS?^5|; zCvBI$lU?pLbyZ#xRzpkJ-`;D4@?ps$J$k1v_$TuQ8T-7@V)|V-nX1#`bDz89kRwVS#!^45a z92^`}08O*9UJ+-OYL~X3*<-0mf-Dz*KI~oWVwOkAAb7NrGPH95h}A)Ynng2mXV-JQ{JU<@;HjKG8w6ZG<&_1wll%g zT)M~m@9}s%J?pXW6GpKnnRB9vFMT;Pcis2@bk)Hqy3Kf@+kBJD{@_OX=terGVxb=* z08}XwSB?%P8U1*G?PM&C^rN8~H=UraB>H6r3_h07G(FtPwP9~u3b=GcxC(Sv{%vY^ z)!;2r-ocxaOGbn&0XTn?y_bd?Mpwl!I|8QD{SM+6#yTEL~(#bUw>BL zco@5uzthg-=k<$z=XfILMexgUFZ#S!+5Z#$Gn#Jpl_Dg>|LF6MK19@i>hpmPt;y-sQH&tJL5IIAtisLiF>f4`Fk+h8IK0Lr=%v9QrWx z4#DT&59^q9Lr%=Hfe(jQ!om2sG#7mQud|9i?1_;BKJ4w(-#E8HgL8c;Xv?`AG&nbJ z2W_0Mh%Rz^KMB6&5WSL#oa(QFww#{^jhy=3pe^TTqKlm7=fSs}_ku=F?-xN^&ig?l zr~2!lEytw(a1ngb{d0<^kh6I_g`CafDdcP%Pl?Z_@l@n&98Z?>>EXulROGyHJY@_i zxT=VC#Sl4n9yO`o=!aK_4RvUuofFTZ_Ka1>p}jV9-c)4|K@A|vI~_Lncd=zte^0br z!%k!9PnW!)0JhiR=s@M*fH5cnF)%leQ$`o$OFY_=O^_Tj?XF2wAia+ z4W$_GH$&9{=UF{yylf>OT=O!mRo*#~(wa(=n_8e4HH9S(aX7ookb7Dha# zoRLsJ4|82@jL1=qmAZ#HFIwlauGTW}uvga7mdW~AwLvmBUq7qn+pnyjRbz}{i~NGy zr8z$NAv`;OtB=>K_&ev0pF|(?-w~}owvqKS&(c@L+$lNcvwluF%%eKyVU2Pw^C){J zbfY;2wcoG;8|=r5kK?E({4M-C`MK{R#OBsPhq+ERcC%U?@gbR33-EW#dW4wBwMK~p zk1cUv?1+Q&np4xRRx%4+NzYJ9BNb$qig z%KE`xNO@?xM7puQ{MPxe)JglV*!kz+Kh_!4TKt#(F8f;h690)G>1A56tnc(+nXA9H z{L8gKIrn(!{HuEt*TYyhO>Enr^xvnO*J0H|o?ExVtk;jZ?2_onr?Mw0d@xnb>~c#4+t9&xwb+z9Z6MUwz=9jYw&rSF*<%o2nuElvvR|huspVr?xz>d9R#tY9G zmvx;Ln>%mHwZo=+kBq-_*n3$2)(AzsUlyI~ZG5w^MlEw5xDU#lTCleNEQT@q_)7(dMkH-1&7T>^YRh(vQz6`!OdA0zprr2Q#RM|1rRhF}r zgRi>kn(aSjE$fcu%&p}3{2b`6B{nUG98dZ2ik|fxE4LZH)Liq){p?@P_{}GNlV8sG z&5tww(4V@dKIRiU{bE;~-$$QTeC4urBi}_^=PN- z>*SYt&wOG$`Q(X+jIOYqU(O>fIqT-)RW+YZ}OqQ z6a1(nw0xeynTubo!))6;M#Sd02|n?aa*3l}>EDW0AKQc8s&he@d7$eeiw-FZy%QTAq&^;x&l4Mb-YYoaXYt(!dnQFD`K3PQlT$gC+)*F% z$(?*w8-2{j&cyMGXvGDO@!MDN-3OZ!3vwanD{L0u{YBbl21HCr?x5*k)VdU(3GWLR z)sy?;`wRp7Q&-zxY{33$u?_pvPW&mpb>g8JtJt4*Vt?9+{p#cX*q^>y_Idha9*bq4 z=U9B6c!5QZ1&honk;Z~VjX7G^-!LqOC8k=Goy*naFXw9V!A1RDn}`e1xUm8k(XLJ4 zl6-K<{!rvF12Qj$TDz}|5v?znTj&d}>f`s0@~7jCxR6}?_Mr5EGhfe^=aJMS)*zub zu^_(nZiBW=VnKXTxa>o(I*g0`m|>gy`t<<%7n|L`*laoUqceaJr?hX2&C;{8S!A{EFzT*(J6Y{K@%5SLaHtdIk1Ki(FHO%_W}AZ~Zso z7Te<7^svS@+3dgb@r-ZvlNsAtZ0>xk{x+|~w&NcG5cnvuNW7P^NdB^KlfTZl*=O0e zId{a~H|)o^={K-Qx)m)OY#Uf4A1snCShx>ZBp)o2E_v=gV3B;0#)7?g*fNW~Wh^rG zma#}#ktd-mvE7vTBAzoomu=2DvQ?jXJCl!{Id)Y%Q%|!d=Ul(6cdkRI$@vUyNIr3p zeB>p6*|+I8Y|H*-4mTvreop@{+nhMIb8_%)+F5*SI9J)sIx=*at#6iXPW+Z_PX4mZ z$zQfPZCJMXS#>LIEBWkr!_Iy`=D$m#2ZhGwoS#?Oi7pwF&+={R{i^I-);s(#TR#n( zTs_8{CiDy*Gi&e&o?(xEROjH~pXe8v$y+L7GkAnX^usT~Bg^`J8nq*NQ@l;qs7*t-)KgwOL_n^jowwc#F0M z4_oUMwgzv})_R4l(Qnb#;4RwP^E22x4@`-$)mYx`kXKl}+QXbcFMD`LyhW>5c{rX< zoeN~CIy&sqSmv+bQGIlvB_93=-izVH*u|Kr8}$3rI4z>Xmfhj-tirh=`E_jtOi8Z`vkS4>z`&`6m1y zD@H7&5`=R@=jZ=zcuhHep&cqys_ zx@n^~=Orrfd3fpDk((ef=zmZ4gTzh`iiSUS^eLCcyNmJKG~d_lF}*u4MYJ$Vzu(sv zaj1WdB`UoXyPD`1Ifs<+7x6`=#oh26WAR+nrgizn%%RS1q0y_M_F-y@NLuHS>5_hsS%K3p}@)ioF}&#p98KcsyoLJU&}n z1P_nTaBed-^ayWIb?pt$YS!1!WPSsWix;wTUUdG=;yLU>%UC(mpA`)a9cXpcQ~98R z*A@RdkXj;2w~3yhthsrN8dVqc#mQh$ARS{JNA72RO|@}XBhWnLIgWf6%|i-E2W5|2 z7qP4_EAx2El@f}+lKRs6YV5h{D|xK0!@|^@D4l7(h;-mP(zxfvmvup1^%pdEjF>mW zJdj56^CImSv6R%DIKi{D@Gg-YPoG80H`@j@76@+3asDjL^-sDrWERt8Xz&7kG9%(< zAS%#@_bkj~Y>8?0W3?4A5|}fV2HvbYvz}?3vXS_S-hn>41^UDim8PkBVuVKUN)+f; zXoP&CAaaX*Th@qrHrDPo>X|A^DG_|}tUQzYc0>Wz)YWUUI`D;}VGT17ey0V#f#tsX zEXM=9hywf^_=0}m%iU##Mmqdd_FgB70RFovkLT;wX4$ zBO3%jf;P*Du^md3;9Tmab^D3I5IfmdpTeFUA~ZY z%c067DI7l*n=jlq5uTVcwlZSk#nRtj($m z^3G@^VWANt&LWRCU_qN9AGwTI!ed1@NkF%#6{}|WuWtszhE-K4>K=OLzoxu;p=qw) H`uP6=gU}qq literal 0 HcmV?d00001 diff --git a/RandomizerCore/Asm/z2mario/music/Palace.dnm b/RandomizerCore/Asm/z2mario/music/Palace.dnm new file mode 100644 index 0000000000000000000000000000000000000000..78d6be3d33b2ef648f43e761cfd44c5cecb38fce GIT binary patch literal 28420 zcmeI4ZIc{Vb;rA>=XquyJON{fvD>l;0*qN80XuPmWP2S{AfZU&Mc%PxZ53OVWoor5 z@*(mO@+O}k-z8P~E>t-M`Tx$j|K8TFJV;Tg7@xV&+2_u&0Q8Cutas*$Z%uiv_T^Ox85uisAJaO;1* zT~!y5)~NgL!T7`GzcmQVgwU1a-?VSvJ-GJ)(GL8Cb&OiMvB7U00(0T?_QC!8?TrU_ z?;iqN0tl{Uth;>!FXbA^N69C4lbWKAvB7Q~0$}&f@xlFH)4~(GMLkXAHkq{^0Sfo0 z2Pf@2cR#v+cn2gCz-8T)?mFn#EB_+Ql4k(;i{BjHqh13(ee8&?YQejKZyf@__sW$9WT4z zR=t659co=x0h8ft-+6FCJ|=`vTnOOD()5KDqtpHT@Ze5+<>W*UuUsaAer zX8eP8Z(A3cgI}vT*Hrwy?(WSjPj5Gm?i_M*PUsrW&p9^GtwVrK8ald$go&FpgzBiLR$IDY@vFat+@kex9$IIcs0CYYJ112wO%GY@w*_&r_$vhQ~I zaiZ2Ku**RvW2}q7rk2@nZ-4Y*s|@^-Nbeje!LniW8K}G z>|-xh=C!v@9-N#U9zU@vl1&>5g2eac)3dhroFdUj(=K+LMDe7b@y?DmJqmaLQM9x30bYLw-z^2sU;6 zRtXe02(ClG2b|t3o0p8Dz$}@#L2*3-JPmV9>>ih2Hiq>Jb$3i$7l-xwzX-GJ9A}Q& z?Tv%ulcR&XZHH!uo}<1%@u1Kw!Ze6-MGw&c5f@=V_PG#QM#E2RFvfBUa#fgt1~y za1}cp0jBUNkhyr_rRTZm81;Oy9lv;miGr8oH=Mc(U}rn=vqWFM{#u+iEjuG+P_^K% z*T%Nh!YyOwn>R@av49m@Z@+wGB6pk(`9@cuFEj~Td#ioD{G9+Gq zjpd2-sl;}?VH9iOniEZa=_4w07P9}Vs}4rdt@8!3{!qw%AA|DIgacD?&`%WrsuIbo z$PN`5ax4&jaxa7OV_+JSQJ~RD^vetw{0R+%K|@%%4%i3L3b>4nh%V4w#cxx)s{z5} zs+ck$oZa=HinoSbhvF)Jnbf-^n%!m1b97wBvIBz2WM1JLSJ458L248SNc8a+^+|;B z>+GlfG~d@x`sw@0oF~E0_dV%)|DW>j>EFFIEPX>SI``^$(L84o51NXjDt&7RW8f<7%`unN;KL=`^Xy&TLjqr}KF=o2jqO z=jsbVo#Sn7p}9G;Mt$ST?c4h|ulwK-`}KeSJ%*i*=nT4?Hq~hU<5iT%Vz4!&4(ZNK zvlXsT{ZsMvQ86c(;M1zgz41oS_p$-{UfPxKW&iTMY*La5ooqtVGms5++H^U3B9DfG zH)L8-vDO=_!9~$t>u*F4i=CH{DRzE4Wx~$a;N#;Pt7b3x;o}cP``*7rrri6Zl!<%a zNSUw!_`*&x-x4j`!p?t6I%I;>U)=lSa33n>%p zf?eS^^hL|l7Yo0lFWP?N%et>4Q|#YMnXX4#4>9n%^hoO|xfVDO59@iWr-vSCJtAYU z$FxaSGyJ)l@vvTUvG5}vvtl3dDExErDEx@WxZry>9@axVyzbd}cwORQJ!j)#J?*KU zbMde}#KZE$!|R@nhu0+@UiWM~tcQ3M`N7$EcwORQJ!j)#Jraq*lNUHJbo3w1^*bil z2Rz~d-#yheNF5Y@^i=T?t-x1w?2+U+XM(wEIdZ(&3e{lR47a-dem=}Ss};Z4(2(O^ ziySX&Rp4koxoq3O%Qk?KZ6HszR@e}ET5JFp+1Bml%HOb`I~nBLWe5$Q5oko@`L+lg z;XJ}TN+c6}7(V0BdpP?bPu@?<-VEm80-tpjh0n8*WP%?(sPH*DN+$T=ZvE^pmZull z9v+0OpE|Jp^myAtkFq?^t+t=|SwBy!mM2GSKW`zdpBUSIUOLzw;%#}pyR-cf`{EY| zYRl8>Y|qQ0t-no~!1)zq3J&Cl*L^i*0-uYb74MM$Mo*qvoEH8!Qzqoer$ym^D`kRD zjavRYDHD8h&icRGlc#oV&-YR$ME+`L>a#wG8WcHU4A>Memg^&pwJ z9|rrGKH_6-pLp@lYda(V}gYBmtEKfby9_qpRsR!FnJ=h-V!Sd9D?WZ2BpL(!7^$xbp?$v|ksfTf|9wZa^P!F?SJxC__)Pv=z2gw9KdT^1a9xP8i z*dFS^`l$!wKt0$V>cR5VgYBmtY(Mp2dFsLTP!HBmJ=lKg!S+xOmZu(UKlNb!)Pv=z z2irqESii^^I77^-~X)rygt%^cR5VgYBUn zte<+Y{nUf)p&l$xJ=lKg!TPBO%To`whkCGnkuk_YFl6Y>)M)Gc%CWz0eNhBa@X?Yt7vudRrvu%>L)L!`U*;C&4#@6b0&GY&=+F* zsK`t6$;)OqpfoG;(tPq#@{v#ErTOHg)N=TL%1a{y)7oT~smQ3)$cTxCYDw*IXjwugzk_mn*x)~;)eQh{g#4^;M zWx&OJaIt=T7C-dhv-oju_*``2r}f~c^@t2V@fG_SnhQVSjfx#O==-1I|E;GL&AR%i$Pw$Irfe%%jEfAtXj*J5xWu~PVwt^^2|hU@e&AB@ zinYK+{E&GRE)-7IU&d!8P?AU8$u|8_+=51-^B-KyXVQEys*5~zY8fyxAB-$d48~?(4+AaPQ+YO%V+YIXA)4*G`7x4jOJT+r}n{>!y_oS?K zN%XYP_+a_HgVA(<{0oZDdG{Va1KWjHUnj=)7dDfEDg_coD%3lkkK z)Ue)=g~8QjATt(mW&jJ>9p?(_!ajhD*8msQZP*Gf{XXfvC-Wj@TFKZ4u<(AvUSfIn z58KW8m9==T;EZAUz^M2ME|Liwek9s^6=zD@#yOI`2R)fDDKB>5!t;x9;eO5%G)VWJ zKLsP(0Y=`(vR*Pi;3Ao@yG=Uc7CBP*;6ks}M+GjHKZ}d~{Iqf5JYihG&$xJh|5I?` ztcE|}B0b?hxHvDs#b>ov^s>l|IJXA0_oVSiAfS`9nEP5dYu>c}9DgsU}ybc zCw|C)onv{n*El!9(eVREV+~%;P4E&wbczhTI5(N0N4_{afmc)KR}|!q_k1w&e#t#P zFM!LacLs4k#((YqsHj^0>g_d)LGm+Sa*xE%rtHUiJ$d}GJh@<*%!P;dS7OH>#<=It zxaW`eaQs=8_3+30KmPb^lev`pDE`R*NBZNl4F1?p{4t-nnU8Pg!=-L{zZ8n~DgWYo zY~mnXF)ikdF?)i~RR#NyX~j1-tfBRGcclc(f{qf;M@Nm8oQ}eNJR_dOZnhzC+8GW)AV_HvlpDRx)JWYko^_N9~o-$Ze z*ekh2HBaWc(7ni^DdmXmV(^@JSf9LB6l+w~tD^GWi^1bwqO0HCio%zaP?p@-t#3dcOiJZxN?2a0^FpWPoH=-J!=N5Mxqm&Kb)J||4@u#nex%>?kViDW@nAFqJR~CX$au1Q z5>#gQ`M%r?+?VS>l|0XTUkrWBwn1?(v6)9dBqEcp8?$azJ=~2`Gf}FeY9mTv3uo$F zBfJR<#EP84VtX_0yvZ75Zq|9j?Acvr{0MLI4!p_PHYj_Ow&BgJ^CmSv>`mUA-f%D8 zq-MNH3U5+QM0mq#;Y(a-Ou}AblN52w3c{~MizwwRJt1ecSeZprVosDMfhv9_uMNt6 zrIv4sms;>J*N6&Q8?V%dhm)vC{7RmP@Qc$sb}@37!LQt5@n+iXnYsTWJB?EyK4iAw z!!)9a4~w{?g%>9PGA;FCdFsRR)VCLM^nD_d`mi_kVQ=aa5%%(AW_!n>AA9xNXECu? zr&Up)pOHj1Pp#Nn2U^%W?(EHMX+zGKr#|dWeb}4&_Cn6~ratUVeb}4&M1;Lqf(?vO zXYVo)#NJ-73X6~(b@(ph4(uIu_9lXD$l2c1hrOu}dlNzIO?}{-`mi_kVQ=aa5%zML z_5Sem;Y;^CMLN;9_=5FwZ(7GRXt;5_Vitx3rhWfzQoW<9=;-4hF*zsQKs5WM~7q9c)OWiCUre^%g zS-eTjc#{;~MEJm2-g`!Bjp`12pFMBLKTu{o8{UvfDC3s{|^2i%m zq{P5B+3`ngBVXLV>YAJ;3b`h8gRqEPO5W3BIVbZj_di3+CyGN>GVzL&c?FemU^e4G z2gx{4`=x$0`*=CRY5m#D{nHMmoO@*M_s{6rWwS$(X=GJuAgfgILQDUNa=*;LlQ|+l=X^U9VZ?X7MQF>Oq`J{b>(>gI=WE?U|L?oj$5_L98+u~(xpxW-C=q~Kl0L#<`mPsX+ProkHb2CE_mPrH&;HP|%1+yYc0xrOnf0Eym zPx(!LGw<{6vwO<6hnaB+DD+uQ_wHT$?6c3iFXx=9l5qsNtJgl?zSEw+ef`Uu z+gtsrx_J81>GPMXSsRLaj$eZrtEy_zlC2KwuhrjQE}Z*VU$e&bPiMa#9P#1V(;u9@ zw4=Wf`4m4dZ2!Bxd;8WuWoljb>Zb0?#>-bOeRBTng)1>TCidTBRrM3()oLbKdus9H z>#AD*w<`jNgx1BIUtasXJ$>hk_Ty{!@OfZZK&-{twfpjt5Eu!vkG8K~Z%@A6o1xb$ zAt=rgz55|RzMi>r_3me6y(4cYeV5qX9|5rY^UbTbKBtNYb`ANewv4;`1)X3W3#7l@ zP~p3B=Z4n|e>62IyoB%mai^;*0_pOtt9Q1+k3~2ZjebQMFY((GzllKl@anzx%+1?( zx39B=1b~Bj3E&=?8`uJ%^Y_B-8+W(K<^XW0O(Jbs0QZ1#Dv%(5LAXcnCtQH=i<5_UF#HSz$MwhvF2iq+0AkT___gn>cF-Fy z(c4dIv0{MUoA0%ooPr&E^|)FAK5mojf&LRNK*hVbedk*1#ba#-;3xpBRU4N8-VcGD z`gi%W+js7*&k$TG2r9kr*ME7H5cGYQb2q=dx6Mt=$l*!JVP*Id!22OU3FHj&@fX{- zj?CbzOZ+Y2yB~Int=WCQw@)6Op*JnTr|t9}-Jfs)>YsOht?S?Avw#2O?1eLD^O*&E z>woAd{DQu-t$(eyzIJGJI$0TyhJ$|7^UW?kh-KQUnhEaXm|4paUwcuHryV`CRMAs1 zBYv)hRuu6!gm--7g=daE#ZL~D?p62DN_!58vD!#&s5VgRt2JspHQz^aXCyb@dgC>^ zqRB>CL=QFMu&@0FQtN8Ie?mVGI9@|wHh!-MNo$ZF*S<899_V?Vp;pRuQ~fB zR}s?JXas{Eluw?r&@hh1BJMWvXiQGbx`NH1d0CJ{&IYb}qy}w;j_Ze}E2BYES8xr9 zuT{e#40_dw_0ok#e{AQ%n)UeVs{n5{POs+BO7l4NxVVvpfp!G7<8OFHvm@|meXa^| z4-SLhdwpZfBDvl8d+B?W*U#w@;Vj=|2%+|={s;vy{*V5)^yjt{7({pjsC{C7K=2RA zf)VL3UZG8BQyS^GdWg10Bf{4&p1yMB?4=8t>5j?&{WS-Q?{wi>wdKfc&E5j$YD0fb zKO**oXob0DUK6bhF#VS3aiQPt$iLH(|6ND^{f@jyHwL4yN857h^O0ja4a(Tqn`5&X z&avsA@;?)uw*I0c|9nTj6)n_V79Q3-nG+`<+G=}X4bq>(#a7iR z$H{(e9WAgG`k$u>+$Uck>ECNg9Vgi9B1#Pw0)JJ6@#Y?H=cqM+uDzEygz<*l! z%ym^l@bQssvV6ptB1?^1>hTD@+Oo_7UwN!na=ZCz(gcpLN+$D=9II8D z^;Nyz;lCkT{TeS1T#w5l0Pcs#+|9TkcloZ24H|LQl*;^buZxA{ zqt=BUa7lX#E|$+YLIX7{=LtsRVvpZ5DRxrZBxC)tm5Xs(aaYQGHxMcI6ih9DBKV#c z81;*O;xWf19#cQ@n0oL>>L&)1UwmWztP81!7)*KAh?MWfBkM)Vm7n)X#=sW#AIiSA>2lRL9-><(@e!u>n9MIp>1NwV-K!4YRsq<&Q{!aP*`aARO zApRy#BL|cp4f%JpJYn8DzcqcQymPlcf-2WOm1XnE(bZnZbGeqh)ZvqVnL6#>cRtqiCje4;aq;hI)z&@3n7&mK94RS)X*D-QJGUj7gzaM-qrabw9 z9({RlAsNe4yT(PH+Le09$<#wUNXGKmH7)X-Gg1$Ghtw12jF4x7{XvY&t>e_g9x~;5 zC08=mPd!Zi)Wejg9;Q9iL&;b^*0a$6bjX;`o-^nBjOf%u%}hPi%$zSZGw~$$b6&0J zNx|*Kkg@&^(WAnDDP+v2t~LdqmqW(<6Cq>#e;G37Q+r26p8cxEGN1iw>S4bs8S|;n zlOoUhDjD zdsrh9H}WC%kPoSce8~Bd4~ZxFka|SA5o=pP+ONxe*oot1K1BXMN%=s%UFO41U0<#b zPxku2o`W@n#=5^;AC~I_vG7COFHHtVeP5sb(r9p0TjaIcXKF|7T8}-_q<_>$UG_+$ ze#j8tI*n_KzsC08=K;Qbh~T&0C_SdDoB{F~Q|#}n!^d<0VDGMzi^chneD9b9*mI|R z?58EK0|0yFly5`6IVL6SaZ{fCE6=cwX+Z+(l-~?_O!0G@lxM$~&%9zUI5Xe#VH4V! zkN3-q=7Zh^^YLwfMe{*^!F;%lvv5A}7t9AZ4>TVf#kjPluh$0E?7V8rV;i7q%Xa&t z+VY$&sQVl|1LG{F)!uyOEZU;%0YqC*U>5fZJ^!X2b8f?*8rQMJnMlv=EE-sZJ+1h4 zv28O3B_$2t+TZNTS}v2C1FM4Jy@qAkyPB=ryrqG#s|-OKCojF2;h?oF9bT;#n! zazQfY6C*k&8z#qpxz{VU*CcStb$9b&o+_Tdy#GP8xxo4+)oyMK>Sl){iY23N+DB^mo zI9(Li;?LrGSYo-AjY&Uf+)rt&i0i8QKH{2$ikxO0|3+Lp*1$jcrB;|veMo-vj~FCo zGd9^PXUtL`Qa=2-6SG;bO1@h^Ii9i1oKuhUw-2X7E*C>mjDp*LuLZE_%RPeB1nf z@GT?KxMOJIJ}G@HJh%QV!hKNY!1|-U)(h@~qG!LjtM3eV^|c;w?-f1ZF8&O6@h#Je zKP@u*#a(@8xT~-AfcvQE*)Q(uJHuUlt;aE5^nkngGu$(t#5V2(ICG?p`>Xb6!JX8N zhb`rqMu~S{reChbV6DF9gSGg!gOAK*?y2UQ9x)y=j4i(VvhHiWa^HyY$kR6Xme~-k z$7hx~E?UotoTFeTzGc8pe9LT#)^je)5U1i>hB(dVUxC+7O%UHQ#A%Kt(s*(2nfq9@ zVbKdvIW`!H@39$F+W?OZM&es97>REgKGfGT;3B?dBKJcEjB*YI7t4?@Ifqts>H(LO zeyUMFiMF(ukM!AP`Y1&SI<9%3_X+?&i| zYFtNrB0L^MtSw_oeh?n`oAzUmsmw?6t{X?ZYjDswB4>CeLesl4L!R$C@D-m&lsLpu zf3Ve~wv;$BpJeJc>tp!DqG)%E99(yj=S@=<)tkdNY9hJ0-LVZ&xehILka%TT{k5A{oY>mg6mhN$AI%DW5(NE6;_l z7xO;II>I1bA0&U?2g#=n)&;Ltbn?MEeFoOa2j9eKGx)O__g!L>_*us0_Zyqs8_jhi zeMoG|&$AemKkYXsYgvPcDdedy7A$;XCu1b^Mqgql|vi#55hK{W%de9d^Ll0=hpl_S^7yOpS{us@^_>&Ecb$0XY<`1ayj2qA(y*nTPGqs z{n7H>8ECHYhrP7A=^GYg^-h3ko@HkraLV^&i0R~mQ@$&}v*zTtqVruEp0OsMd88iB z;>j0jdnf?xpJ+PE+YWq}W8kBl@BDa)$?K9yz2vlTv!31Tkch{2{o;q(4+RhE=%d1e z;EZ$UVm!braVFR5X7DY4!AKcmC}RshrhGSsTJs|oU+l<}cd3WG%f9$GV`H-;a|(Wm zV`47l$(NKDX}_~35$m)?e*dA&GxKJCp3Q=JcJG%lXWcKeZwZ|M;6rJ^ly=Cr;SQC@a z*)aLtTG?GUh>6r6F%j#spW_zWh>4UD>6qaBxQvM%e=K8yz1uP-T49hmx{QhVe>kaU zw8Uk(c0`?j($)^{1ys6klmFO+Zv+w-=I@;S_{lZLiN0C8dU{7dU;BRkK>s`g(XZd@ zaa+k}BCXwA@;xHy^&Y@`gF?5uwps_D^?F)p*565?v1z5yB5f~Yvd^K7>KKFH<08jE z*@y8`S1UTlV7wgL>wU4QU|?G%;_=vP<#-Qce%z;EA5D8e^Rr*rlPFFc9_&d(Gu86G zWW2k>Zv@z5=DB82kL+WFr}ZIY1wMm%Y?}RL@?i{nL-XmMeD;n>vv*7yf2AJp5sUuT z{E_vr|4VuNnDW%WV?ETH_p)bdKKCW}4*gTGNBd=(=FF&L`dsTgK@yz-5^6B#zKZ<`_i>sD6 Id*Ors10q5L2LJ#7 literal 0 HcmV?d00001 diff --git a/RandomizerCore/Asm/z2mario/music/Town.dnm b/RandomizerCore/Asm/z2mario/music/Town.dnm new file mode 100644 index 0000000000000000000000000000000000000000..85994da7c19fa26c6ed62540de7165b675e2d3c1 GIT binary patch literal 19331 zcmeI3dv6}q5yp4-z3qFkow}4bP+%JnDp5*l`vnq$Q=&E@jm`hHh$}3DgM1PF6#cW` ztKSBR3i|x!**)uZ?5!h&DwUn^dgshJXU@zsb1r+~qr>0a-}~}l_h|34FZPeBhtHp$ zKHGmh&a%gMpWJ=8lO>vpdqcl+mPA>Wi&xw1ntqjj4<6nBM3+R=|236o zY4wl9zCv;L`PYZKZ|r6{lRBuc=a7G3ckgKL_;VIh zb%*5ZLx4O3z)!x~KV-fJe7bCi&vL=r*LkP~zCWHGoa`M{4>*%K`^Nba_@Dv4J_MM) ze>(W=i|WzygX4X`Z1Qgfcu-&Wp%%yjz}=$*Ry^n5T0X#crMv;A2EOXv=?VF02;nFQ z;45kR$u?u9`}6+Z)9UWYiEdvh8o{57(X9v@@ql0-0wiG|_@KIf@b~?vlnr5X|0Wpw z6b!X%3Bm`}@AU5j1c9NhK@L^CoAK|f57t$14!)MhvnKxb^&V=udwX*5 zbf1&6p&N02HVx4AA;6}wbGUc3?{gpzGL2yU1c4_Bef@{ppLG$e9_}4|_Bq19Q5Ui^ zXuz=#0Tw}Lq7KxXDmV`}2K=rz0oivGew?Uv3QRdjWl&!PhPljsyZijhN*T!OlV&er zOAZ>)>qCGU40`cI;TI|xKOPO%)Yp5+KK5c|UbS;_dUCRV^s0(RaC3O5b+t8+17Q15 zE1s-gPbkI+0a}_~hvH%l*TXElHe;HHbXr>Qgb)z6j!t1mdeX z=j@;X#6ARG1o1VYR%c?)Av?>}rccFC`yz<1ClIwd@~HyvoCXm45a4lw&NlUD3cpMM za>%Bj3EkIysO4}|zpmSP_~9S;l*;pXlykEeG$7cAKy$b`^SI_s!4@d$QfK0jY5fRr zC(IEs-73G;V3=>^>4?}DhkpG(g<8(k=Z@LcgoX;l0Mbw-H)~P5bqOoWsDw+-M4C2iib1l?_Q}c2*odkx>#0dnJJWYX&2P+u# z!uZ_jLZcXeRB+PtM&M*N0QLEwoXc<$`vU=DJrK+ zk|7n#RlEt8D2#va?KiHI2vN_U>^rg`HJ=jqwz!HMptiov{r1}|`{tW0o6T0M*(~0T zs50XCEfi5t+R7!7rWh^Wzs>MOx^1x@PZuRxHgirq`K3$D%z3o^w@L{{@l}&AG&@TQ z*>^!qA5}Dg$vEg+1+Z(1xD?r;Nk%&c2tVlyG5sJ+qcSFFtR(s>0|CF#P!uD=N;#rX z#8N;pG9p%iREqCXQ>h@BlxkB7!kNlN7EcT*hvF>0N;*>{nyHv`4;{rAwjh{Px`c0( zVg(=u)+h;(=<;IrhyRQskLi|HN z)_5}NpZa~EUrm}anjpDmP^O<&{7gSBs+oS>I_(P7O{Y`pX0ur?Nm-V;B;C?{J})HE zj>AQpPo`$|Ma#ul^^foF?ruMM)SMDxbN*YiKh&mSEtHS0k2hm3m80vL80;3K;BJbW z21h%Hh%xn9su!c{)6MuK!TngL+?v@niGFNQ=2&#hq1D{46DKB!3`!ez=#Ncdyd{_! zcRjl!<*~|-x5UxbGRDLrM?#zgjBbm#r<{(^Qi*d9dxw^Hl1xkxQDxhy{jBk^)_Npe zKx!2uq2C<3%-u?ym>>{ujxfS(yjfV+*0s%eUa%Np4OW{2Gjm~oc#PF$aG*C0j*&*e zi3wr_ZtS0HBqP;EN3z;}#}56YF|w}xmRYCKXpVX6Cvd>Wu_Pwe1uB!k+L6YmhOmj4 zfrN5Q`GW@Ag@#4gKnG_av&yxBO4NdiYaLarW~5crf{JTF#eLsTwPJ#tOHJvHjKHE* zfv$5tC5v2(MfMnr%4Cr_ESiNZvMV?codkz%1+v>AOqZpM_o zGL|v5zSLvRVjq3c65RdIUmZ?@UGN|?4wfOE@e0MFrd(5sKuEh@5 z5+PgJ%^ufAgu(|k3u1gS2r@=kX|3J3w04-o4$qQGoFmET?L1?Jg&O_JwbHAzG2@Wq z)*xmXej0^VKQXp`{PdV&Vg+XmvJ9)0nbIF?MPwoNJ6i+gkhLR4UIiA6)av(&2YPK} z8Kuy#x!aP}8VRZOd-N@Z8eiL$eG)4fqqi&jRDGS_?u#8B-F~8_Nn7M*iUOupbz>((((F|m=?Bpw><33yY_InAGqN>2 z22ro);Zw-Mli=P~?J77>UIfSJ-V4NpcW2jqyz^Y~YTrf?@7jIK-rw(BBWvCuIG0k>mrS|%p+ zqx*tAYVw*?qV>$$o%X=424+TMA$jN3$nnNjqlDi^2{eU8=@r)V5Cl9D2y|y3JsbVn z?`r*y0{X3$R^AQK?+A8hw?{&Mch0ezf+Vzz)q|XL8?<@{YG-%s8j;t|MKZiQrnE@L zNcNpbSl=yiuI=t)%yLF(PCK>(@9YV7xF0b&dm6bRWASr5x}mB{+%!1GEQ2HZ?VK)D zf||Qv9ktaDsoUVY9(hT|=z5eS4)k4%9g~n@Z9byb>(!lWuRKwqmZ9A}K*subQes)m z+|Uv;H@GU*f?FA~casbsRv`mHJqOZIAZUGJ(%t%He&LO^UgnK;wLTDM_h`K#;`SfE zj9u#&-nd64&ie4iJ&1^9cw_AF#yxJQTKsYkG2s`{Y5X#lZM^Vn5y*7?nud%hwSE~l z{Bn=3U+!^Hye^B9dvyKs82IHex_-HbD#I`Kmc}pFbp0|$UB9fg>(?~$A?9UpUBBD| zzl?M@UbeSVZ5uCVTGy{KG`HE;^~*h^B>Z9yHGVmgUBBG=zx!1rzb3tYou6Nq#moM8 z{W?FZ`ut*_X#Bcryyh3=q2o%t9Jy}1T>C@g)#jnc==$Ya84&Se4mEzAUk_hq-7vnF z<)LeT=(@2?tDrNh>z8XAzc@RrQxG{v$jo*<@XX**tYw$24CiMM>$iKbk{QeQlI*fM z(#71_uTCO+(zVTb*|p8GZnjtks)LBsB}?oB z_FDF$y`2+RLL%E*_;OLi2Ii0>;dmlzajc@Y>xm=UP?Xs#cvsUz1f*(cW0iNz0KzM`@!gRymvS`O}Z!hXGfE@$hm%J(CH3cE){6^ zIf`?Wa{+Wn;2h=$rFb$^g6V z)6w|&Ff{JaspL^)-g{*X@Y6Fm*q`vY>#+@$I`UUGaqg{xq3y|dKUrBNi^`tQ8V~f% z*(+m!>=;Njb(bq=XD8%y;Yiwm`CC60V3N1XpcI_m;u zC1mK-jbU4+Mi37PAprzenaU#Fjb;N2!w~TxP-Ky&qm#pbz3s{~|0b^dJQ2*}au2gN z_1}2b@Q7#}h{yLkqft8O83S^Y{wn4VyrheF!>#ehLN2r?*~rH(ZX+`$SBp5M5s;Ef zq?a?71V-#N=rQnkIi?Pb*h%DKhHpRzM(o_Qmf_oq5j%e+p!sE;+lmqU9q5(JepfMK zzejtv{sYB`J%MiVJW`Cu;(i|0cD4^@QRC#sU`ydIb%^I4Dh8ub81@T>=FDg2hz!|Kg?)H8fpJ%Ew* zSr3b+tR5B*>tXSf)x+XpJuIHGdRRQHhs9G?4~vKOuy`)l0||3n1(QN7Y=R7{_b4o1pxXO~8J~=zy!@%mOjpw5xI95PyJOjEDSyo#DP2(Vw zYGW_MhV{sn+Sso_#CXYw+N38VM$W9&COM1kBW(D}F%SsjXD{C50ODn>OF0PoQVwF9 z&7m%A&E}x%&E^2xwQ|t>-y$ky=RGv710!YB6sz9F7mprV`IDJa@d2`BPKseBl`w)$ zzCPk*)aoV6>Rq8GUUI8ma;sh_5(9U$o#HEYiu0Y+JG+y5xs!TlcTz8R(vG_-TY8wS zT?ExIY_$XC2n~IKG^sV6OH_k_{i9y)tX?R>5#PqC_KtaMCw34|K^ScG*SpEE)9r8T Mb;fxzH|=fw1&=mcI{*Lx literal 0 HcmV?d00001 From 3b33440c6739376006af44eda1567b1c9edd3f79 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 29 May 2026 01:40:48 -0400 Subject: [PATCH 065/128] Start trying to fix shadow sprites on boss defeat --- RandomizerCore/Asm/z2mario/boss.s | 18 +++++------------- RandomizerCore/Asm/z2mario/integration.s | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index c691074e1..2693e4768 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -693,13 +693,15 @@ BossJump: ; DrawMetasprite lives in PRG0, so we save the current bank, swap in PRG0, ; do the draw, then restore the bank to bank5 after BOSS_OAM_OFFSET = 48 - +.export BossGraphicsHandlerForceDraw BossGraphicsHandler: lda boss_animation bne @active rts @active: - + lda #BOSS_OAM_OFFSET + sta CurrentOAMOffset +BossGraphicsHandlerForceDraw: lda NmiBankShadow8 pha lda NmiBankShadowA @@ -715,20 +717,10 @@ BossGraphicsHandler: lsr bcs @restore @draw: - lda #BOSS_OAM_OFFSET - sta CurrentOAMOffset - ldy #1 ; sprite slot 1 (Y for DrawMetasprite & SprAttrib,y) lda #2 ; palette 3 attribute (shadow mario palette) sta SprObject_SprAttrib,y ldx boss_animation - - cpx #SKIDDING - bne @nm_no_skid_flip - jsr DrawMetasprite - jmp @nm_drew -@nm_no_skid_flip: - jsr DrawMetasprite -@nm_drew: + jsr DrawMetasprite jsr BossDrawProjectiles ; shadow fireballs + hammers (palette already set per-slot) @restore: diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index cc7b0a667..1e7b3d076 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1534,4 +1534,19 @@ CheckIfPlayerSmall: .org $8F8F ; U N L I K E L I N K .byte $23,$14,$0b,$ee,$e7,$e5,$e2,$e4,$de,$f4,$e5,$e2,$e7,$e4,$ff -.assert * = $8f9e \ No newline at end of file +.assert * = $8f9e + + +; TODO: The following are locations where it draws the "shadow" +; behind link after defeating a boss, but idgac right now to fix it +; waaayyyy more work than its worth atm. +.segment "PRG4" +.org $978F + jmp *+3 + +.org $AE08 + jmp *+3 + +.segment "PRG5" +.org $9C8E + jmp *+3 From a9358d9dde181732279a5acc3f2755703a9ecf40 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 29 May 2026 02:57:04 -0400 Subject: [PATCH 066/128] Fix shadow sprite and remove music workaround (will update z2ft to get it later) --- RandomizerCore/Asm/ram.inc | 5 ++ RandomizerCore/Asm/z2ft.s | 24 +++--- RandomizerCore/Asm/z2mario/integration.s | 94 ++++++++++++++++++++---- 3 files changed, 98 insertions(+), 25 deletions(-) diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index 543e6d27f..c46877f75 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -631,6 +631,11 @@ RESV boss_ChrBank RESV boss_transition_delay ; nonzero = boss is paused mid-state-switch (frames remaining) BOSS_RAM_SIZE = RES_OFFSET - CUSTOM_BOSS_START - 1 +; After a boss dies, the game renders a shadow during the flashing animation +; and this is our custom value to say "hey its time to copy the mario sprites" +; and also doubles as the direction to cast the shadow. +RESV MarioShadowXOffset + EnemyProjectileOffset = 7 EnemyProjectileType = ObjectMetasprite + EnemyProjectileOffset EnemyProjectile_X_Position = SprObject_X_Position + EnemyProjectileOffset diff --git a/RandomizerCore/Asm/z2ft.s b/RandomizerCore/Asm/z2ft.s index 3d82594c2..7a67985e4 100644 --- a/RandomizerCore/Asm/z2ft.s +++ b/RandomizerCore/Asm/z2ft.s @@ -26,9 +26,9 @@ UpdateSound = $878c jsr UpdateSound ; clear out the moa fire work around - lda $07fd - and #~$40 - sta $07fd ; Noise channel buffer + ; lda $07fd + ; and #~$40 + ; sta $07fd ; Noise channel buffer pla sta NmiBankShadowA sta PrgBankAReg @@ -58,12 +58,12 @@ UPDATE_REFS SwapToPRG0 @ $c130 ; Work around how moa fire works. It queues a noise sfx with writing $40 to $eb ; but doesn't set a buffer for it. This kept z2ft from knowing that the noise channel ; was in use, so we set the value in the buffer and clear it after update. -.org $95A3 - jmp FixMoaFireSfx -.reloc -FixMoaFireSfx: - sta $400C - lda $07fd - ora #$40 - sta $07fd ; Noise channel buffer - rts +; .org $95A3 +; jmp FixMoaFireSfx +; .reloc +; FixMoaFireSfx: +; sta $400C +; lda $07fd +; ora #$40 +; sta $07fd ; Noise channel buffer +; rts diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 1e7b3d076..f9b43249c 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -193,8 +193,27 @@ DrawFallingMarioSprite: nop .segment "PRG7" -.org $dd1b ; TBird draws a flashing mario sprite here without banking - jsr BankPatchLinkDrawRoutine +.org $dd1b + jsr SetMarioShadowFlag + +.reloc +SetMarioShadowFlag: + ; Cast the shadow horizontally opposite the boss (the light source). The boss is + ; the enemy currently being processed (slot in $10). Boss to Link's left -> shadow + ; to the right (+8); boss to the right -> shadow to the left (-8). No Y offset. + ldx $10 + lda LinkXPosition + cmp EnemyXPositionLo,x + lda LinkXPositionHi + sbc EnemyXPositionHi,x + bpl @bossLeft ; Link >= boss -> boss is on the left + lda #<(-8) ; boss on the right -> shadow to the left + .byte $2c + @bossLeft: + lda #8 ; boss on the left -> shadow to the right + @store: + sta MarioShadowXOffset + rts .export BankPatchLinkDrawRoutine .reloc @@ -269,6 +288,14 @@ PatchLinkDrawRoutine: + pla ; restore PlayerFacingDir sta PlayerFacingDir + + ; Copy the sprite data from mario into the data for the shadow if + ; the flag to draw the shadow was set this frame. + lda MarioShadowXOffset + beq + + jsr BlitMarioShadow + + + lda $070f ; nonzero if shield is cast beq + jsr DrawShieldSprite @@ -531,6 +558,48 @@ Yhi = R7 .endproc +.reloc +; Quick copy the rendered mario and place it behind him to create a shadow mario +.proc BlitMarioShadow + SHADOW_PALETTE = 2 + SHADOW_OAM = $50 ; shadow is hardcoded to sprite 20 (offset $50) + ; Copy the rendered mario sprites (always at OAM offset 4) into the shadow slot. + ldx #4 + @loop: + lda Sprite_Y_Position,x + sta Sprite_Y_Position + SHADOW_OAM - 4,x + + lda Sprite_Tilenumber,x + sta Sprite_Tilenumber + SHADOW_OAM - 4,x + + ; keep H/V flip, force the dark palette + lda Sprite_Attributes,x + and #%11000000 + ora #SHADOW_PALETTE + sta Sprite_Attributes + SHADOW_OAM - 4,x + + lda Sprite_X_Position,x + clc + adc MarioShadowXOffset + bcs @clear ; check that the shadow doesn't go offscreen + sta Sprite_X_Position + SHADOW_OAM - 4,x +@nextsprite: + inx + inx + inx + inx + cpx CurrentOAMOffset + bcc @loop + @done: + lda #0 + sta MarioShadowXOffset + rts + @clear: + lda #$f8 + sta Sprite_Y_Position + SHADOW_OAM - 4,x + bne @nextsprite +.endproc + .reloc ShieldSpriteXOffset: .byte 24 ; Overlaps with Y offset table to save a byte @@ -1537,16 +1606,15 @@ CheckIfPlayerSmall: .assert * = $8f9e -; TODO: The following are locations where it draws the "shadow" -; behind link after defeating a boss, but idgac right now to fix it -; waaayyyy more work than its worth atm. -.segment "PRG4" -.org $978F - jmp *+3 +; The following are locations where it draws the explosion +; after killing a boss +; .segment "PRG4" +; .org $978F +; jmp *+3 -.org $AE08 - jmp *+3 +; .org $AE08 +; jmp *+3 -.segment "PRG5" -.org $9C8E - jmp *+3 +; .segment "PRG5" +; .org $9C8E +; jmp *+3 From f353c7e0ecc3292b6fc6a674cc792b766e8d8da5 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 29 May 2026 11:37:19 -0400 Subject: [PATCH 067/128] Fix cutoff sfx so that it properly kills sfx Setting the queue to 1 will stop the current sfx, and i was skipping that sfx accidentally --- RandomizerCore/Asm/z2mario/integration.s | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index f9b43249c..a37b6496d 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1300,7 +1300,7 @@ ClearQueues: .reloc CheckMarioSq1Sfx: lda $07E1 ; L990B active effect type (non-zero = L990B is driving Square 1) - bne @Z2ExtraActive + bne @ActualZ1Sfx jsr Square1SfxHandler lda Square1SoundBuffer beq @PlayingZ2 @@ -1315,7 +1315,6 @@ CheckMarioSq1Sfx: ; but now we can clear it. lda #0 sta Z2Square1SoundBuffer -@Z2ExtraActive: rts @ActualZ1Sfx: jmp $92F4 ; Original square 1 sfx from z2 From 7f82ca1ab1b161b6a61efe3035435121db471742 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 29 May 2026 11:37:40 -0400 Subject: [PATCH 068/128] Allow the speedup patch to run for z2 mario mode only --- RandomizerCore/Hyrule.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 9d1310def..4981634bf 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -4006,6 +4006,17 @@ void PatchAddress(byte opcode, int before, int after) PatchAddress(STX_ABS, 0x6381, 0x6341); var asm = engine.Module(); asm.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.z2ft.s"), "z2ft.s"); + + if (props.MarioMode) + { + // There is a patch in z2ft to reduce offscreen enemy checking overhead + // but for the randomizer, it counters out its own lag reduction by adding + // additional forced lag to counterbalance the lag it removed. + // For z2mario, there's just so much more stuff going on, and people don't + // have the same expectations for the game, so this will "unpatch" the + // extra lag routine, so that it uses the lag reduction code. + rom.Put(0x1ffec, 0xb4, 0xf2); + } } UpdateTexts(engine, texts); From 66afee82e9c42b16aa95fdd00bb02c8a4d02fce8 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 29 May 2026 11:38:12 -0400 Subject: [PATCH 069/128] Fix tail swipe issue when using it while silent It didn't properly mute itself before --- RandomizerCore/Asm/z2mario/sfx.s | 1 + 1 file changed, 1 insertion(+) diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s index 8e6e31ed8..ed67d51c9 100644 --- a/RandomizerCore/Asm/z2mario/sfx.s +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -173,6 +173,7 @@ ContinueTailWag: @streamEnd: lda #$00 + sta SND_SQUARE1_REG+2 sta Squ1_SfxLenCounter sta Square1SoundBuffer rts From 394cb01a484e33c0befa6be109744df48dfe778c Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 29 May 2026 15:55:56 -0400 Subject: [PATCH 070/128] Fix tile collision for bricks added later --- RandomizerCore/Asm/z2mario/integration.s | 55 ++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index a37b6496d..75f8894fe 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1137,6 +1137,61 @@ FREE_UNTIL $e6f3 ; .org $93ba ; rts ; patch the link main routine to skip some unneeded stuff + +; Update the falling block "set block on ground" so that it updates marios +; tile collision memory too +.segment "PRG4" +.org $AC34 + jsr AddFallingBlockToMarioCollision + nop +.reloc +AddFallingBlockToMarioCollision: + pha + lda $da + clc + adc #<(COLLISION_TILES - $6000) + sta $da + lda $db + adc #>(COLLISION_TILES - $6000) + sta $db + pla + sta ($da),y + ; original code + lda $2a,x + and #$f8 + rts + +; Riverman has a custom bridge building routine and we need to update the +; mario collision to match +.segment "PRG0" +.org $A809 + jsr AddSariaBridgeToMarioCollision +.reloc +AddSariaBridgeToMarioCollision: + txa + sta ($00),y + lda $01 + pha + lda $00 + pha + ; now also put the tile into mario + clc + adc #<(COLLISION_TILES - $6000) + sta $00 + lda $01 + adc #>(COLLISION_TILES - $6000) + sta $01 + txa + cmp #$56 + bcc @notcollision + sta ($00),y +@notcollision: + pla + sta $00 + pla + sta $01 + rts + .segment "PRG0", "PRG7" ; Update the position of the brick to remove the mario collision box too .org $A700 From 7d3d3976f1ad5508712f7592c820fbaf9398e245 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 29 May 2026 20:52:41 -0400 Subject: [PATCH 071/128] Allow hammer when small with upstab --- RandomizerCore/Asm/z2mario/mario.s | 8 ++++++-- RandomizerCore/CustomTexts.cs | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 88588718c..4809692b7 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -2505,9 +2505,13 @@ GetProperObjOffset: bne + jmp UpdateHammers + - lda PlayerSize ; Have to be big to throw hammers? + ; Check if you either have upstab + lda PlayerSize ; or are big enough to throw hammers beq + - jmp UpdateHammers + lda HaveStabs + and #$10 + bne + + jmp UpdateHammers + ; and that the player is pushing up + b lda SavedJoypadBits diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index b55c9f256..2d562d666 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -719,10 +719,20 @@ private static Text GenerateWizardText(List texts, Random r, Location loca .Union(WIZARD_SPELL_TEXTS_BY_COLLECTABLE[collectable]).ToList(); int selectedHintIndex = r.Next(possibleWizardHints.Count()); return new Text(possibleWizardHints[selectedHintIndex]); - } else if (z2MarioMode && collectable == Collectable.JUMP_SPELL) + } else if (z2MarioMode) { - // In Mario Mode, change the JUMP spell hint text to reflect its new ability - return new Text("JUMP WONT$HELP YOU.$TRY THIS$INSTEAD."); + // In Mario Mode, change a few spell hint text to reflect its new ability + switch (collectable) + { + case Collectable.JUMP_SPELL: + return new Text("JUMP WONT$HELP YOU.$TRY THIS$INSTEAD."); + case Collectable.DOWNSTAB: + return new Text("HERE IS$HOW TO$PIERCE$SHIELDS."); + case Collectable.UPSTAB: + return new Text("UP B TO$THROW$HAMMER$WHEN SMALL"); + default: + break; + } } //Non community-text spells use the vanilla text corresponding to the spell you get. return new Text(texts[wizardTextIndexesBySpell[collectable]].RawText); From a31932e0b085ac8396ab063e7769c47862aabe91 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 29 May 2026 21:51:48 -0400 Subject: [PATCH 072/128] Make downstab pierce enemy shields --- RandomizerCore/Asm/z2mario/integration.s | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 75f8894fe..dd17bda37 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1527,6 +1527,33 @@ CheckForDownstab: lsr rts +; Update the type of enemies you can stab through to hit shield enemies if you are +; stomping and you have downstab, then you can pierce shields. I really only want this +; on large enemies that have a shield sprite, not bosses and such. +.segment "PRG7" +.org $E64F + jsr DownstabAwareShieldCollision + +.reloc +DownstabAwareShieldCollision: + ; If you are stomping + lda PlayerIsDownstabbing + lsr + bcc @normal + ; And you have downstab + lda HaveStabs + and #$04 + beq @normal + ; check if we want to stab this kinda enemy + ldx $10 + lda $0444,x ; enemy vulnerability. type 2 is the boss shield kind like gooma? + cmp #2 + beq @normal + clc ; downstab on a shield style enemy + rts +@normal: + jmp $E9F9 ; continue the shield collision check + .segment "PRG3" .org $9A42 jmp * + 4 ; skip a hardcoded metasprite update when reading signs From 8e47743e4498fcdf7b0d16c18802252506529720 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 29 May 2026 21:52:35 -0400 Subject: [PATCH 073/128] Properly clear projectile processing flag if you died while throwing a hammer it could keep you from bouncing off enemies --- RandomizerCore/Asm/z2mario/integration.s | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index dd17bda37..29f91eddd 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1010,7 +1010,7 @@ SetTailSwingHitbox: @done: rts -; Prevent recoil and playing the sword sound when hitting a well +; Prevent recoil and playing the sword sound when hitting a wall .org $E268 BCS $E28B .org $E273 @@ -1080,7 +1080,8 @@ PatchFireballHitcheck: @CollisionCheck: inc ProjectileProcessing jsr $E694 - dec ProjectileProcessing + lda #0 + sta ProjectileProcessing bcc @NoCollision ldy $11 lda Fireball_State,y From 393f45492e622e6dd2af24c39023434e4ccb49f1 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 29 May 2026 22:24:12 -0400 Subject: [PATCH 074/128] Force the swing version into east/west as I want them to be --- RandomizerCore/Music.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index 1e176d347..1cf23dde3 100644 --- a/RandomizerCore/Music.cs +++ b/RandomizerCore/Music.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Http.Headers; using System.Text; using Z2Randomizer.RandomizerCore.Overworld; @@ -213,6 +214,19 @@ public void ImportSongs() WriteLogLine(null); WriteLogLine("SELECTED SONGS:"); + // I want my songs in a particular order for z2mario, so uncomment this when making the romhack + if (false) + { + var regular = usesSongs[Usage.Encounter].Find(x => !x.Title.Contains("Swing"))!; + var swing = usesSongs[Usage.Encounter].Find(x => x.Title.Contains("Swing"))!; + selUsesSongs[Usage.Encounter] = + [ + regular, + regular, + swing, + swing, + ]; + } foreach (var (usage, usageSongs) in selUsesSongs) { string songNames = string.Join("}, {", usageSongs); From 5e4ab72016f79d03873ad728ddbc974cf47de1aa Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 30 May 2026 02:58:26 -0400 Subject: [PATCH 075/128] Fix up stab handlers --- RandomizerCore/Asm/z2mario/integration.s | 19 +++++++++++-------- RandomizerCore/Asm/z2mario/mario.s | 2 +- RandomizerCore/Asm/z2r.inc | 3 +++ 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 29f91eddd..9f1980177 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -953,9 +953,12 @@ SetPlayerDownstabbingHitbox: lda HitboxYCoord cmp #$f0 bcc @notstabbing - lda Player_State - cmp #1 - bne @notstabbing + ; shouldn't need to check for "jumping" state anymore. + ; i did that originally cause hitboxes were jank back then, but i think + ; i fixed those bugs so we can bring back walk off attacks + ; lda Player_State + ; cmp #1 + ; bne @notstabbing lda Player_Y_Speed bmi @notstabbing beq @notstabbing @@ -1533,22 +1536,22 @@ CheckForDownstab: ; on large enemies that have a shield sprite, not bosses and such. .segment "PRG7" .org $E64F - jsr DownstabAwareShieldCollision + jsr DownstabPierceShieldCollision .reloc -DownstabAwareShieldCollision: +DownstabPierceShieldCollision: ; If you are stomping lda PlayerIsDownstabbing lsr bcc @normal ; And you have downstab lda HaveStabs - and #$04 + and #HAVE_DOWNSTAB beq @normal ; check if we want to stab this kinda enemy ldx $10 - lda $0444,x ; enemy vulnerability. type 2 is the boss shield kind like gooma? - cmp #2 + lda $0444,x ; enemy vulnerability. + cmp #6 ; type 6 is the type goomas head uses. wanna bounce on him still beq @normal clc ; downstab on a shield style enemy rts diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 4809692b7..86d16c2bb 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -2509,7 +2509,7 @@ GetProperObjOffset: lda PlayerSize ; or are big enough to throw hammers beq + lda HaveStabs - and #$10 + and #HAVE_UPSTAB bne + jmp UpdateHammers + diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 54215276c..8368b2c65 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -90,6 +90,9 @@ LineIrqStatusReg = $5204 DISABLE_SCANLINE_IRQ = 0 ENABLE_SCANLINE_IRQ = $80 +HAVE_UPSTAB = $04 +HAVE_DOWNSTAB = $10 + MulLoReg = $5205 MulHiReg = $5206 From bb6fe9668a51023942d54324178e0719c366286c Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 2 Jun 2026 16:33:54 -0300 Subject: [PATCH 076/128] Clean up csproj files and use a consistent style --- CommandLine/CommandLine.csproj | 64 +++---- .../CoreSourceGenerator.csproj | 66 ++++--- .../CrossPlatformUI.Browser.csproj | 179 ++++++++---------- .../CrossPlatformUI.Desktop.csproj | 110 +++++------ CrossPlatformUI/CrossPlatformUI.csproj | 156 +++++++-------- Desktop.Common/Desktop.Common.csproj | 48 +++-- Directory.Packages.props | 68 +++---- DumpRooms/DumpRooms.csproj | 6 +- RandomizerCore/Music.cs | 2 +- RandomizerCore/RandomizerCore.csproj | 10 +- Statistics/Statistics.csproj | 2 +- Tests/Tests.csproj | 16 +- ValidateRooms/ValidateRooms.csproj | 6 +- 13 files changed, 327 insertions(+), 406 deletions(-) diff --git a/CommandLine/CommandLine.csproj b/CommandLine/CommandLine.csproj index 4b8af794a..e0f5b9269 100644 --- a/CommandLine/CommandLine.csproj +++ b/CommandLine/CommandLine.csproj @@ -1,35 +1,29 @@ - - - - Exe - enable - enable - Z2Randomizer.CommandLine.Program - Z2R - AnyCPU;x64 - Z2Randomizer.CommandLine - default - Debug;Release;Unsafe Debug - true - - - - true - false - - - - - - - - - - - - - PreserveNewest - - - - + + + Exe + enable + enable + Z2Randomizer.CommandLine.Program + Z2R + AnyCPU;x64 + Z2Randomizer.CommandLine + default + Debug;Release;Unsafe Debug + true + + + true + false + + + + + + + + + + PreserveNewest + + + \ No newline at end of file diff --git a/CoreSourceGenerator/CoreSourceGenerator.csproj b/CoreSourceGenerator/CoreSourceGenerator.csproj index bed7384f5..df16071f5 100644 --- a/CoreSourceGenerator/CoreSourceGenerator.csproj +++ b/CoreSourceGenerator/CoreSourceGenerator.csproj @@ -1,34 +1,32 @@ - - - - netstandard2.0 - default - enable - true - true - false - false - Debug;Release;Unsafe Debug - AnyCPU - - - true - true - - - true - false - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + netstandard2.0 + default + enable + true + true + false + false + Debug;Release;Unsafe Debug + AnyCPU + + + true + true + + + true + false + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + \ No newline at end of file diff --git a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj index 84d4299d0..c75d9c6d6 100644 --- a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj +++ b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj @@ -1,106 +1,81 @@ - - - Exe - net10.0-browser - browser-wasm - wwwroot\main.js - ./_framework - true - true - - true - - $(MSBuildProjectName) - - Z2Randomizer.CrossPlatformUI.Browser - - Debug;Release;Unsafe Debug - - AnyCPU - partial - - - false - false - - - - true - - - - full - true - true - false - - - - full - true - true - false - true - false - - + + + Exe + net10.0-browser + browser-wasm + wwwroot\main.js + ./_framework + true + true + true + $(MSBuildProjectName) + Z2Randomizer.CrossPlatformUI.Browser + Debug;Release;Unsafe Debug + AnyCPU + partial + + false + false + + + true + + + full + true + true + false + true + + + full + true + true + false + true + true + false + + + + + - + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CrossPlatformUI.Desktop/CrossPlatformUI.Desktop.csproj b/CrossPlatformUI.Desktop/CrossPlatformUI.Desktop.csproj index 6065e2a2b..52438a895 100644 --- a/CrossPlatformUI.Desktop/CrossPlatformUI.Desktop.csproj +++ b/CrossPlatformUI.Desktop/CrossPlatformUI.Desktop.csproj @@ -1,61 +1,51 @@ - - - WinExe - - enable - true - true - true - true - true - true - partial - true - default - Debug;Release;Unsafe Debug - AnyCPU - - - - Z2Randomizer - - - - - app.manifest - False - CrossPlatformUI.Desktop.Program - Z2Randomizer.CrossPlatformUI.Desktop - 1st.ico - - - - - true - false - - - - - - - - - - - - - - - - - - - Always - - - Always - - - + enable + true + true + true + true + true + true + partial + true + default + Debug;Release;Unsafe Debug + AnyCPU + + + Z2Randomizer + + + app.manifest + False + CrossPlatformUI.Desktop.Program + Z2Randomizer.CrossPlatformUI.Desktop + 1st.ico + + + true + false + + + + + + + + + + + + + + Always + + + Always + + + \ No newline at end of file diff --git a/CrossPlatformUI/CrossPlatformUI.csproj b/CrossPlatformUI/CrossPlatformUI.csproj index 9c261a452..5f0314aac 100644 --- a/CrossPlatformUI/CrossPlatformUI.csproj +++ b/CrossPlatformUI/CrossPlatformUI.csproj @@ -1,74 +1,78 @@ - + + + enable + default + true + false + true + false + $(MSBuildProjectName) + Z2Randomizer.CrossPlatformUI + Debug;Release;Unsafe Debug + AnyCPU + true + + + + $(NoWarn);IL2026 + + + true + false + + + + + + + + + + + + + + + + + + + all + + + + + + + + + + + + + + + + + + + + + + + + - enable - default - true - false - true - false - $(MSBuildProjectName) - Z2Randomizer.CrossPlatformUI - Debug;Release;Unsafe Debug - AnyCPU - true + false - - - - $(NoWarn);IL2026 - - - - true - false + + true - - - - - - - - - - - - - - - - - - - - all - - - - - - - - - - - - - - - - - - - - false - true - - - - - @(GitTag->'%(Identity)', ',') - + + + + @(GitTag->'%(Identity)', ',') + // (auto-generated from .csproj) namespace Z2Randomizer; @@ -83,12 +87,10 @@ } } - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/Desktop.Common/Desktop.Common.csproj b/Desktop.Common/Desktop.Common.csproj index 1d8311df8..5386b312f 100644 --- a/Desktop.Common/Desktop.Common.csproj +++ b/Desktop.Common/Desktop.Common.csproj @@ -1,26 +1,22 @@ - - - - enable - enable - default - Debug;Release;Unsafe Debug - AnyCPU - true - - - - true - false - - - - - - - - - - - - + + + enable + enable + default + Debug;Release;Unsafe Debug + AnyCPU + true + + + true + false + + + + + + + + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 064599182..e5b865550 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,6 +1,6 @@ - true + true true 5.3.0 7.5.1 @@ -9,57 +9,49 @@ 3.119.4-preview.1.1 - - - - - - - - - - - - - - - - - - + + - + + + + + - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - + - - - - - - + + \ No newline at end of file diff --git a/DumpRooms/DumpRooms.csproj b/DumpRooms/DumpRooms.csproj index f51f9abd9..45425db10 100644 --- a/DumpRooms/DumpRooms.csproj +++ b/DumpRooms/DumpRooms.csproj @@ -1,5 +1,4 @@ - Exe enable @@ -11,14 +10,11 @@ Debug;Release;Unsafe Debug true - true false - - - + \ No newline at end of file diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index 1cf23dde3..bfb69993f 100644 --- a/RandomizerCore/Music.cs +++ b/RandomizerCore/Music.cs @@ -215,7 +215,7 @@ public void ImportSongs() WriteLogLine(null); WriteLogLine("SELECTED SONGS:"); // I want my songs in a particular order for z2mario, so uncomment this when making the romhack - if (false) + if (true) { var regular = usesSongs[Usage.Encounter].Find(x => !x.Title.Contains("Swing"))!; var swing = usesSongs[Usage.Encounter].Find(x => x.Title.Contains("Swing"))!; diff --git a/RandomizerCore/RandomizerCore.csproj b/RandomizerCore/RandomizerCore.csproj index 6c0013f19..66a9d0db9 100644 --- a/RandomizerCore/RandomizerCore.csproj +++ b/RandomizerCore/RandomizerCore.csproj @@ -1,4 +1,4 @@ - + Library false @@ -15,7 +15,6 @@ true true - @@ -34,12 +33,9 @@ Always - - - @@ -49,7 +45,6 @@ - @@ -61,7 +56,6 @@ - @@ -96,4 +90,4 @@ - + \ No newline at end of file diff --git a/Statistics/Statistics.csproj b/Statistics/Statistics.csproj index 5ed9a1bbc..4119deecc 100644 --- a/Statistics/Statistics.csproj +++ b/Statistics/Statistics.csproj @@ -1,4 +1,4 @@ - + net10.0-windows Exe diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 905b9b64e..e98418b9d 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -1,32 +1,22 @@ - - + net10.0-windows enable enable - false $(NoWarn);IL2026;IL2027 - false - AnyCPU;x64 - Z2Randomizer.Tests - $(MSBuildProjectName) - default - Debug;Release;Unsafe Debug - true false - @@ -37,10 +27,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - + \ No newline at end of file diff --git a/ValidateRooms/ValidateRooms.csproj b/ValidateRooms/ValidateRooms.csproj index 55cf6cf8a..5c0e88db1 100644 --- a/ValidateRooms/ValidateRooms.csproj +++ b/ValidateRooms/ValidateRooms.csproj @@ -1,5 +1,4 @@ - Exe enable @@ -10,14 +9,11 @@ Debug;Release;Unsafe Debug true - true false - - - + \ No newline at end of file From 3919da3c0796bb7b5dc5429c42a3ade9734e952b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 2 Jun 2026 23:18:12 -0300 Subject: [PATCH 077/128] Fix tanooki getting stuck and player palette on sideview change --- CommandLine/Properties/launchSettings.json | 2 +- RandomizerCore/Asm/StatTracking.s | 6 ----- RandomizerCore/Asm/ram.inc | 8 +++++-- RandomizerCore/Asm/z2mario/integration.s | 28 ++++++++++------------ 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/CommandLine/Properties/launchSettings.json b/CommandLine/Properties/launchSettings.json index 38f4ce70b..d85a269e3 100644 --- a/CommandLine/Properties/launchSettings.json +++ b/CommandLine/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "CommandLine": { "commandName": "Project", - "commandLineArgs": "-f AAABvvyhAmLAAAAAAAAAEuPk+hAAAAAAAAAAAAAsA -r ../Zelda2.nes -s 32165874 -o ..", + "commandLineArgs": "-f AAABvvyhAmLAAAAAAAAAEuPi+yAAAAAAAAAAAAAWAA -r ../Zelda2.nes -s 123456789 -o ..", "workingDirectory": "../build/" } } diff --git a/RandomizerCore/Asm/StatTracking.s b/RandomizerCore/Asm/StatTracking.s index 6f2bf7883..d049fcf0e 100644 --- a/RandomizerCore/Asm/StatTracking.s +++ b/RandomizerCore/Asm/StatTracking.s @@ -3,12 +3,6 @@ .import SwapPRG, SwapToSavedPRG -BUFFER_OFF = $0301 -PPUADDR_HI = $0302 -PPUADDR_LO = $0303 -BUFFER_LEN = $0304 -BUFFER_DAT = $0305 - DOT = $cf SPACE = $f4 diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index c46877f75..f4d1686f6 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -296,8 +296,12 @@ Sprite_Attributes = $202 Sprite_X_Position = $203 Sprite_Data = Sprite_Y_Position -VRAM_Buffer1_Offset = $302 -VRAM_Buffer1 = $303 + +BUFFER_OFF = $0301 +PPUADDR_HI = $0302 +PPUADDR_LO = $0303 +BUFFER_LEN = $0304 +BUFFER_DAT = $0305 LinkXSubpixel = $03d6 Enemy0XSubpixel = $03d7 diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 9f1980177..e0e64ac05 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -344,6 +344,13 @@ PatchLinkDrawRoutine: .import METASPRITE_BIG_MARIO_CROUCHING TANOOKI_FLUTTER_CAP = 1 ; SMB3 PLAYER_TAILWAG_YVEL FLUTTER_DURATION = 16 ; SMB3 Player_WagCount initial value + + lda TailSwingTimer + beq @runSuit + ; count down the timer if we are already swiping + dec TailSwingTimer + rts +@runSuit: lda $d0 ; Tanooki suit active? beq @done @@ -379,13 +386,6 @@ FLUTTER_DURATION = 16 ; SMB3 Player_WagCount initial value sta Square1SoundQueue @noAPress: - lda TailSwingTimer - beq @maybeStart - ; count down the timer if we are already swiping - dec TailSwingTimer - rts - -@maybeStart: lda A_B_Buttons and #B_Button beq @done @@ -669,7 +669,8 @@ OverworldLateInit: .org $8D61 ; patches over an unused write for $0727 in sideview init - jsr UpdatePlayerPalette + ; jsr UpdatePlayerPalette + .segment "PRG7" .reloc UpdatePlayerPalette: @@ -684,24 +685,21 @@ UpdatePlayerPalette: ldx #7-1 + ; we are still firey so write a palette update for next NMI - ldy $301 + ldy BUFFER_OFF dey - iny inx lda PlayerRegularPalettePPUCommand,x - sta $301+1,y + sta PPUADDR_HI,y bpl - - tya - clc - adc #6 - sta $301 + sty BUFFER_OFF ; Check if we are still using shield. lda $69de cmp #$18 ; Check if the current "user" color is the same as the original color beq + ; if not, then replace it (this is the right address trust me) - sta $301,y + sta BUFFER_OFF,y + pla tax From b8ef08d5ac1b6f4f91647d99a5a204fb9a26dd4a Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 3 Jun 2026 00:15:34 -0300 Subject: [PATCH 078/128] Fix issues with game over screen --- RandomizerCore/Asm/MMC5.s | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RandomizerCore/Asm/MMC5.s b/RandomizerCore/Asm/MMC5.s index bb4a6b05f..e95ab1cf5 100644 --- a/RandomizerCore/Asm/MMC5.s +++ b/RandomizerCore/Asm/MMC5.s @@ -679,14 +679,15 @@ Bank1f_Nmi: .reloc GanonLaughingTeeHee: lda #0 + sta $0488 ; clear the default menu selection option sta $0726 lda #(6*2) | PRG_BANK_ROM sta PrgBank8Reg lda #(6*2+1) | PRG_BANK_ROM sta PrgBankAReg - ; re-enable rendering + ; re-enable bg rendering only lda $FE - ora #$18 + ora #$08 ora $0768 sta $FE @loop: From 8ab47ef196def8a938eee84824e55f8c12075ddd Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 8 Jun 2026 23:13:39 -0300 Subject: [PATCH 079/128] Add a version hash for the vanilla z2mario romhack --- RandomizerCore/Hyrule.cs | 22 ++++++++++++++++++---- RandomizerCore/Music.cs | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 4981634bf..db8c2f6ff 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -205,6 +205,17 @@ public RandomizerProperties Props private readonly NewAssemblerFn NewAssembler; private readonly PalaceRooms palaceRooms; + // HACK: A few places in the app need to check if we are using mario mode, and i didn't feel like refactoring them + // to have another bool parameter passed in. + public static bool Z2MarioModeEnabled = false; + // In general, we will distribute the randomizer app in release mode, so this should always be true for players. + // But for z2mario as a romhack, I build in debug mode, so this just removes the "RANDOMIZER" text in the title. +#if DEBUG + const bool IS_RANDOMIZED = false; +#else + const bool IS_RANDOMIZED = true; +#endif + public string Hash { get; private set; } //This entire class structure is hot fucking garbage, but refactoring it such that it makes sense is @@ -223,6 +234,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC { Hash = ""; World.ResetStats(); + Z2MarioModeEnabled = config.MarioMode; SeedHash = BitConverter.ToInt32(MD5Hash.ComputeHash(Encoding.UTF8.GetBytes(config.Seed!)).AsSpan()[..4]); r = new Random(SeedHash); @@ -538,6 +550,10 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC SanitizeHashCharacters(z2Hash); } + if (Z2MarioModeEnabled && !IS_RANDOMIZED) + { + z2Hash = Util.ToGameText("VER. 1.2.0 "); + } ROMData.Put(0x17C2C, z2Hash); Hash = Util.FromGameText(z2Hash); @@ -2424,9 +2440,7 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra rom.UpdateSprite(props.CharSprite, true, props.ChangeItemSprites); rom.UpdateSpritePalette(props.TunicColor, props.SkinTone, props.OutlineColor, props.ShieldColor, props.BeamSprite); } - // Z2Mario, when making the vanilla romhack, change this flag to false - const bool isRandomized = false; - if (isRandomized) + if (IS_RANDOMIZED) rom.Put(ROM.ChrRomOffset + 0x01000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.randomizer_text.chr")); else { @@ -3844,7 +3858,7 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando rom.ChangeMapperToMMC5(engine, props.DisableHUDLag, randomizeMusic, props.MarioMode); // will make output vary with customize tab options // the second flag determines if we should add the "randomizer" text, but we don't have a "vanilla" option // right now, so this is just always off, except when i manually make an update - rom.AddRandomizerToTitle(engine, props.MarioMode, false); + rom.AddRandomizerToTitle(engine, props.MarioMode, IS_RANDOMIZED); AddCropGuideBoxesToFileSelect(engine); FixHelmetheadBossRoom(engine); FullItemShuffle(engine, GetNonSideviewItemLocations()); diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index bfb69993f..42a4b9a08 100644 --- a/RandomizerCore/Music.cs +++ b/RandomizerCore/Music.cs @@ -215,7 +215,7 @@ public void ImportSongs() WriteLogLine(null); WriteLogLine("SELECTED SONGS:"); // I want my songs in a particular order for z2mario, so uncomment this when making the romhack - if (true) + if (Hyrule.Z2MarioModeEnabled) { var regular = usesSongs[Usage.Encounter].Find(x => !x.Title.Contains("Swing"))!; var swing = usesSongs[Usage.Encounter].Find(x => x.Title.Contains("Swing"))!; From c2021055c88ca1777a2031066e48dc2dc752a239 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 8 Jun 2026 23:50:21 -0300 Subject: [PATCH 080/128] Add flute twister stub --- CrossPlatformUI/Lang/Resources.resx | 3 +++ CrossPlatformUI/Views/Tabs/SpellsView.axaml | 6 ++++++ RandomizerCore/Asm/Flute.s | 12 ++++++++++++ RandomizerCore/Hyrule.cs | 5 +++++ RandomizerCore/ROM.cs | 6 ++++++ RandomizerCore/RandomizerConfiguration.cs | 4 ++++ RandomizerCore/RandomizerProperties.cs | 1 + 7 files changed, 37 insertions(+) create mode 100644 RandomizerCore/Asm/Flute.s diff --git a/CrossPlatformUI/Lang/Resources.resx b/CrossPlatformUI/Lang/Resources.resx index b5b193dff..450f54a65 100644 --- a/CrossPlatformUI/Lang/Resources.resx +++ b/CrossPlatformUI/Lang/Resources.resx @@ -856,6 +856,9 @@ dash, the Dash spell will still appear, but do nothing. Link will always shoot beams from his sword, regardless of life. + + +Flute works like the Zelda 1 flute and spawns a twister you can walk into to warp the player to dungeons that they've already cleared (placed a gem in). If you are facing up or right, then it will take you to the next dungeon in order, if you are facing down or left, it will take you to the previous dungeon in order. When enabled, your starting items will be randomized. diff --git a/CrossPlatformUI/Views/Tabs/SpellsView.axaml b/CrossPlatformUI/Views/Tabs/SpellsView.axaml index d39fb3574..bd2e132ee 100644 --- a/CrossPlatformUI/Views/Tabs/SpellsView.axaml +++ b/CrossPlatformUI/Views/Tabs/SpellsView.axaml @@ -80,6 +80,12 @@ > + + + diff --git a/RandomizerCore/Asm/Flute.s b/RandomizerCore/Asm/Flute.s new file mode 100644 index 000000000..fdb3d933e --- /dev/null +++ b/RandomizerCore/Asm/Flute.s @@ -0,0 +1,12 @@ +.include "z2r.inc" + +; Twister warp: when the flute plays and neither a river devil nor the +; three-eye-rock (Hidden Palace) tile is adjacent, spawn a twister that +; travels right across the overworld. Touching it warps Link into the +; interior of the next/previous gem-placed dungeon in sequence. + +; This is just a stub for now... + +.segment "PRG1" + +.segment "PRG2" diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index db8c2f6ff..3c3ccb6cc 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3951,6 +3951,11 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando rom.DashSpell(engine); } + if (props.FluteTwisterWarp) + { + ROM.FluteTwisterWarp(engine); + } + if (props.UpARestartsAtPalaces) { RestartWithPalaceUpA(engine); diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 55f3c03df..fd3becaff 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -2168,6 +2168,12 @@ public void AggressiveThunderbird() Put(0x16413, thunderBirdHP); } + public static void FluteTwisterWarp(Assembler asm) + { + var a = asm.Module(); + a.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.Flute.s"), "flute.s"); + } + public void DashSpell(Assembler asm) { var a = asm.Module(); diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index eafb1c1f8..25b26c2ac 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -684,6 +684,9 @@ private bool palaceStylesAnyMetastyleSelected() [Reactive] private bool marioMode; + [Reactive] + private bool fluteTwisterWarp; + //Custom [Reactive] [IgnoreInFlags] @@ -1458,6 +1461,7 @@ public RandomizerProperties Export(Random r, bool includeDifficulty = true) }; properties.JumpAlwaysOn = jumpAlwaysOn; properties.DashAlwaysOn = dashAlwaysOn; + properties.FluteTwisterWarp = fluteTwisterWarp; properties.FastCast = fastSpellCasting; properties.BeamSprite = beamSprite; properties.DisableMusic = disableMusic; diff --git a/RandomizerCore/RandomizerProperties.cs b/RandomizerCore/RandomizerProperties.cs index ab8d747e0..8a19ca006 100644 --- a/RandomizerCore/RandomizerProperties.cs +++ b/RandomizerCore/RandomizerProperties.cs @@ -219,6 +219,7 @@ public class RandomizerProperties public byte BeepThreshold { get; set; } public bool JumpAlwaysOn { get; set; } public bool DashAlwaysOn { get; set; } + public bool FluteTwisterWarp { get; set; } public bool FastCast { get; set; } public BeamSprites BeamSprite { get; set; } public bool DisableMusic { get; set; } From 3e3f25211fcf2f990598285e9b71b55accfad982 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 9 Jun 2026 22:13:41 -0300 Subject: [PATCH 081/128] First attempt at making the flute warp work --- RandomizerCore/Asm/Flute.s | 241 ++++++++++++++++++++++++++++- RandomizerCore/Asm/ram.inc | 11 ++ RandomizerCore/Asm/z2mario/mario.s | 33 ++-- RandomizerCore/Hyrule.cs | 2 +- 4 files changed, 266 insertions(+), 21 deletions(-) diff --git a/RandomizerCore/Asm/Flute.s b/RandomizerCore/Asm/Flute.s index fdb3d933e..e7cb4b078 100644 --- a/RandomizerCore/Asm/Flute.s +++ b/RandomizerCore/Asm/Flute.s @@ -1,12 +1,243 @@ .include "z2r.inc" -; Twister warp: when the flute plays and neither a river devil nor the -; three-eye-rock (Hidden Palace) tile is adjacent, spawn a twister that -; travels right across the overworld. Touching it warps Link into the -; interior of the next/previous gem-placed dungeon in sequence. +; Flute Twister Warp +; When Link plays the flute on the overworld and neither a river-devil tile +; (OverworldFacingTerrain==$0F) nor the three-eye-rock tile is adjacent, a twister +; spawns at the left screen edge and travels rightward. Touching it +; warps Link into the next or previous gem-placed dungeon in sequence. +; Face right/up = next palace; face left/down = previous palace. +; The anchor is the last dungeon Link entered or warped to (TwisterCurrentPalaceSlot). + +TWISTER_TILE = $E0 ; tile ID for frame 0; frame 1 uses TWISTER_TILE+1 +TWISTER_PAL = $02 ; OAM palette attribute (bits 0-1) + +.segment "PRG0", "PRG7" + +ProcessNextOverworldEnemy = $842F +UpdateOverworldSpritePosition = $8397 +AreaLoadTrigger = $85eb + +; Patches the place where the enemy graphics data is loaded +; so we can draw a custom tornado +.org $8377 + jsr TwisterDrawCheck + +; patch the enemy movement handler (this spot checks if its a fairy) +.org $8435 + jsr TwisterVelocityCheck + nop + +; Patch the collision handler to check if we want to warp +.org $83CE + jsr TwisterHitCheck + +.org $85A9 + jsr RecordPalaceEntry + +.reloc +TwisterDrawCheck: + lda OverworldEnemy0Type, x ; reproduce overwritten LDA $82,x + cmp #$04 + beq @twister + tax ; do the original + rts +@twister: + ; type 4: write twister tile and palette + ; Y = slot*16 (OAM base), set before this hook + lda R7 ; animation frame (0 or 1) + clc + adc #TWISTER_TILE + sta $0281, y ; left sprite tile + sta $0285, y ; right sprite tile + lda #TWISTER_PAL + sta $0282, y + sta $0286, y + pla + pla + jmp UpdateOverworldSpritePosition + + +.reloc +TwisterVelocityCheck: + lda OverworldEnemy0Type, x + cmp #$04 + beq @twister + ; vanilla branch, so do the original cmp instruction and bail + cmp #$03 + rts +@twister: + ; constant +1 rightward X velocity + lda #$01 + sta OverworldDemonXVelocity, x + ; pop this function off the stack frame and move to the next enemy + pla + pla + jmp ProcessNextOverworldEnemy + +.reloc +TwisterHitCheck: + lda OverworldEnemy0Type, x + cmp #$04 + beq @do_warp + ; Vanilla is checking what link is facing here + lda OverworldFacingTerrain + rts +@do_warp: + jmp TwisterWarp + +.reloc +TwisterWarp: + ; Direction: OverworldFacingDirection bits: $01=right, $08=up = NEXT; $02=left, $04=down = PREV + lda OverworldFacingDirection + ; right ($01) or up ($08) + and #$09 + bne @go_next + lda #$05 ; add 5 to wrap around and go backwards in the list + bne @scan ; unconditional +@go_next: + lda #$01 ; add 1 to go forward in the list +@scan: + sta R7 + ; a = current palace slot we want to check + lda TwisterCurrentPalaceSlot + ; x = 6 is the loop counter for the number of palaces to check + ldx #$06 +@next_palace: + ; increment or decrement the list but clamp the result between 0 and 5 + clc + adc R7 + bmi @below_zero + cmp #$06 + bcc @no_wrap + sec + sbc #$06 + jmp @no_wrap +@below_zero: + clc + adc #$06 +@no_wrap: + pha ; save candidate palace_code + tay + lda CrystalPlaced, y ; gem placed at this palace? + bne @found + pla + dex + bne @next_palace + rts ; there should always be a valid palace?? the twister shouldn't spawn otherwise +@found: + pla ; A = target palace_code + sta TwisterCurrentPalaceSlot + ; clear the twister slot + ldx OverworldCurrentSlot ; slot index saved at $80 by LDX $80 / STX $80 in L8332 + lda #$00 + sta OverworldEnemy0Type, x + sta OverworldDemonTimer, x + ; set LocationNumber to palace area index ($34 + palace_code) + lda TwisterCurrentPalaceSlot + clc + adc #$34 + sta LocationNumber + + pla + pla + jmp AreaLoadTrigger + +.reloc +RecordPalaceEntry: + ; reproduce overwritten STX $0748 + stx LocationNumber + ; update anchor when entering a palace (area index $34-$39 = palace codes 0-5) + txa + ; negative: not a palace + sec + sbc #$34 + bcc @done + ; >= 6: not a valid palace code + cmp #$06 + bcs @done + sta TwisterCurrentPalaceSlot +@done: + rts + + +.macro TwisterSpawnBody +.local crystal_check, crystal_found, active_check, not_active, free_slot, init_slot, spider, doublereturn + ; instruction before was CMP #$0F + beq spider + ; Check if at least one gem is placed ($078D + palace_code, palace_code = 0-5). + ldx #$05 +crystal_check: + lda CrystalPlaced, x + bne crystal_found + dex + bpl crystal_check + bmi doublereturn ; unconditionally exit +crystal_found: + ; Refuse to spawn a second twister if one is already active. + ldx #$07 +active_check: + lda OverworldEnemy0Type, x + cmp #$04 + ; already have a twister so skip processing it + beq doublereturn + dex + bpl active_check + ; Find a free demon slot (type == 0). + ldx #$07 +free_slot: + lda OverworldEnemy0Type, x + beq init_slot + dex + bpl free_slot + inx ; set x = 0 and just overwrite slot 0 if all else fails +init_slot: + lda #$04 + sta OverworldEnemy0Type, x ; type 4 = twister + lda #$01 + sta OverworldDemonTimer, x ; any non-zero value keeps slot active + lda OverworldScrollY ; camera Y + clc + adc #$70 ; center-screen height + sta OverworldEnemyYPosition, x ; world Y + lda ScrollPosShadow ; camera X + sta OverworldEnemyXPosition, x ; world X = left screen edge (OAM X = 0) + lda #$01 + sta OverworldDemonXVelocity, x ; +1/frame rightward + lda #$00 + sta OverworldDemonYVelocity, x ; no vertical movement + ; Twister spawned. Return from flute dispatch (don't fall into L838F). +doublereturn: + pla + pla +spider: + rts ; returns to L838F: spider/rock encounter +.endmacro -; This is just a stub for now... .segment "PRG1" +; Hook: Replace BEQ L838F; RTS (3 bytes at $838C) with JSR TwisterSpawnPRG1. +; JSR pushes PC+2 = $838E; RTS inside TwisterSpawnPRG1 returns to $838F = L838F. +.org $838C + jsr TwisterSpawnPRG1 +; The code after the patch point is where it normally branches to if the flute +; is removing a spider or spawning 3 eyed rock +.assert * = $838f + +.reloc +.proc TwisterSpawnPRG1 + TwisterSpawnBody +.endproc + + .segment "PRG2" + +; Same hook at the same address. +.org $838C + jsr TwisterSpawnPRG2 +.assert * = $838f + +.reloc +.proc TwisterSpawnPRG2 + TwisterSpawnBody +.endproc diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index f4d1686f6..4bd143344 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -81,6 +81,7 @@ zp_28 = $28 LinkYPos = $29 EnemyYPositionLo = $2a Enemy0YPositionLo = $2a +OverworldEnemyYPosition = $2a ; [OW] base of overworld demon Y world position array, indexed by slot X Enemy1YPositionLo = $2b Enemy2YPositionLo = $2c Enemy3YPositionLo = $2d @@ -121,6 +122,7 @@ LinkXPosition = $4d LinkXPositionLo = $4d EnemyXPositionLo = $4e Enemy0XPositionLo = $4e +OverworldEnemyXPosition = $4e ; [OW] base of overworld demon X world position array, indexed by slot X Enemy1XPositionLo = $4f Enemy2XPositionLo = $50 Enemy3XPositionLo = $51 @@ -170,7 +172,9 @@ Projectile5XVelocity = $7c LinkProjectile0XVelocity = $7d LinkProjectile1XVelocity = $7e zp_7f = $7f +OverworldScrollY = $7f ; [OW] camera Y scroll offset; OAM_Y = world_Y - OverworldScrollY LinkAnimationFrame = $80 +OverworldCurrentSlot = $80 ; [OW] current overworld demon slot index during per-frame processing (0-7) Enemy0AnimationFrame = $81 OverworldEnemy0Type = $82 ; [SS] Enemy1AnimationFrame - [OW] OverworldEnemy0Type, 1 = Weak, 2 = Strong, 3 = Fairy OverworldEnemy1Type = $83 ; [SS] Enemy2AnimationFrame - [OW] OverworldEnemy1Type @@ -336,12 +340,17 @@ timer_501 = $0501 ; $501 through $50d are timers that are LinkEntrySlideTime = $0503 SwordAttackTimer = $050a InjuryTimer = $050c +OverworldDemonTimer = $050e ; [OW] overworld demon active flag/timer, indexed by slot X; non-zero = active EncounterSpawnTimer = $0516 RNG = $051b MapNumber = $0561 +OverworldFacingDirection = $0562 ; [OW] Link's facing direction: $01=right, $02=left, $04=down, $08=up +OverworldFacingTerrain = $0563 ; [OW] terrain type Link is currently facing TownNumber = $056b PalaceNumber = $056c PalaceRegionIndex = $056c ; Palace index in continent (0-2) +OverworldDemonYVelocity = $056d ; [OW] overworld demon Y velocity, indexed by slot X (signed: $01=down, $FF=up) +OverworldDemonXVelocity = $0575 ; [OW] overworld demon X velocity, indexed by slot X (signed: $01=right, $FF=left) LinkYVelocityHi = $057d EnemyYVelocity = $057e Enemy0YVelocity = $057e @@ -430,6 +439,7 @@ HaveFlute = $0789 HaveCross = $078a HaveHammer = $078b HaveMagicKey = $078c +CrystalPlaced = $078d ; base of crystal-placed flags, indexed by palace_code (0-5); non-zero = gem placed Keys = $0793 ; Current number of Keys Crystals = $0794 ; Number of Crystals left HaveStabs = $0796 ; 0x04 = Have Downstab | 0x10 = Have Upstab @@ -505,6 +515,7 @@ RESV BlockBreakColumnOffset, BREAKABLE_BLOCK_COUNT ; Vanilla addr $042E - Offset RESV BlockBreakAddrLo, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table lo addr RESV BlockBreakAddrHi, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table hi addr +RESV TwisterCurrentPalaceSlot ; palace_code (0-5) of last dungeon Link entered or warped to .ifdef ENABLE_Z2_MARIO diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 86d16c2bb..c7db73cae 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -14,8 +14,8 @@ .export GameRoutines, ProcFireball_Bubble, PlayerGfxHandler -.segment "PRG0" -; .reloc +.segment "PRG0", "PRG7" +.reloc ;------------------------------------------------------------------------------------- @@ -76,7 +76,7 @@ .word $0000 ; VerticalPipeEntry .word $0000 ; FlagpoleSlide .word $0000 ; PlayerEndLevel - .word PlayerLoseLife + .word $0000 ; PlayerLoseLife .word $0000 ; PlayerEntrance .word PlayerCtrlRoutine .word PlayerChangeSize @@ -85,6 +85,7 @@ .word PlayerFireFlower .endproc +.reloc ; ;------------------------------------------------------------------------------------- ; ; ; .reloc @@ -167,7 +168,7 @@ ;------------------------------------------------------------------------------------- ;$07 - used to hold upper limit of high byte when player falls down hole AutoControlPlayer: - sta SavedJoypadBits ;override controller bits with contents of A if executing here + ; sta SavedJoypadBits ;override controller bits with contents of A if executing here PlayerCtrlRoutine: lda GameEngineSubroutine ;check task here @@ -313,9 +314,10 @@ PlayerSubs: ; sta JoypadOverride ;clear controller override bits if any are set ; jsr SetEntr ;do sub to set secondary mode ; inc AltEntranceControl ;set mode of entry to 3 - rts + ; rts -.proc PlayerLoseLife +; .reloc +; .proc PlayerLoseLife ; inc DisableScreenFlag ;disable screen and sprite 0 check ; lda #$00 ; sta Sprite0HitDetectFlag @@ -356,10 +358,11 @@ PlayerSubs: ; sta HalfwayPage ;store as halfway page for player ; jsr TransposePlayers ;switch players around if 2-player game - rts + ; rts ; jmp RunGameOver::ContinueGame ;continue the game -.endproc +; .endproc +.reloc ;------------------------------------------------------------------------------------- ;$00 - used to store player's vertical offscreen bits ; .reloc @@ -372,9 +375,9 @@ PlayerSubs: CntPl: lda GameEngineSubroutine ;if executing specific game engine routine, cmp #$0b ;branch ahead to some other part - beq PlayerKilled + jeq PlayerKilled lda PlayerChangeSizeFlag ;if grow/shrink flag set - bne DoChangeSize ;then branch to some other code + jne DoChangeSize ;then branch to some other code jsr FindPlayerAction ;otherwise jump and return @@ -408,16 +411,17 @@ ClearMarioSprite: lda #0 sta ObjectMetasprite rts + +DoChangeSize: + jsr HandleChangeSize ;find proper offset to graphics table for grow/shrink + jmp PlayerGfxProcessing ;draw player, then process for fireball throwing .endproc +.reloc FindPlayerAction: jsr ProcessPlayerAction ;find proper offset to graphics table by player's actions jmp PlayerGfxProcessing ;draw player, then process for fireball throwing -DoChangeSize: - jsr HandleChangeSize ;find proper offset to graphics table for grow/shrink - jmp PlayerGfxProcessing ;draw player, then process for fireball throwing - PlayerKilled: ; ldy #$0e ;load offset for player killed ; lda PlayerGfxTblOffsets,y ;get offset to graphics table @@ -1170,7 +1174,6 @@ OffscrJoypadBitsData: .byte $01, $02 ; ------------------------------------------------------------ -.segment "PRG0", "PRG7" .reloc ProcessPlayerAction: lda Player_State ;get player's state diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 3c3ccb6cc..cba27e6e5 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -585,7 +585,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC private Assembler CreateAssemblyEngine() { - var asm = NewAssembler(assemblerOptions); + var asm = NewAssembler(assemblerOptions, true); asm.Callbacks = new Js65Callbacks { OnFileReadText = AsmFileReadTextCallback, From a3a91a33ba5b2c67d64484400a6cd2c1c310661c Mon Sep 17 00:00:00 2001 From: Ellendar Date: Tue, 9 Jun 2026 12:32:23 -0400 Subject: [PATCH 082/128] Builds but almost certainly contains logical bugs. --- CommandLine/Program.cs | 1 + .../ViewModels/GenerateRomViewModel.cs | 1 + DumpRooms/Program.cs | 1 + RandomizerCore/CustomTexts.cs | 128 +-- RandomizerCore/Enemy/Enemies.cs | 44 + RandomizerCore/Enemy/PalaceEnemyShuffler.cs | 1 + RandomizerCore/Hyrule.cs | 773 +++++++----------- RandomizerCore/Music.cs | 9 +- RandomizerCore/Overworld/DeathMountain.cs | 42 +- RandomizerCore/Overworld/EastHyrule.cs | 230 ++---- RandomizerCore/Overworld/Location.cs | 110 ++- RandomizerCore/Overworld/LocationID.cs | 27 +- RandomizerCore/Overworld/MazeIsland.cs | 23 +- RandomizerCore/Overworld/WestHyrule.cs | 207 +++-- RandomizerCore/Overworld/World.cs | 70 +- RandomizerCore/ROM.cs | 70 +- RandomizerCore/RandomizerConfiguration.cs | 2 +- RandomizerCore/RandomizerProperties.cs | 2 +- RandomizerCore/RequirementType.cs | 2 + RandomizerCore/Requirements.cs | 3 + RandomizerCore/RomMap.cs | 1 + RandomizerCore/Shuffler.cs | 2 +- ...ranceDirectionItemRoomSelectionStrategy.cs | 2 +- .../ByShapeItemRoomSelectionStrategy.cs | 2 +- .../{ => Palace}/ChaosPalaceGenerator.cs | 4 +- .../{ => Palace}/CoordinatePalaceGenerator.cs | 2 +- .../{ => Palace}/ItemRoomSelectionStrategy.cs | 2 +- .../Sidescroll/{ => Palace}/Palace.cs | 97 +-- .../Sidescroll/{ => Palace}/PalaceColors.cs | 3 +- .../{ => Palace}/PalaceGenerator.cs | 2 +- .../Sidescroll/{ => Palace}/PalaceRooms.cs | 2 +- .../Sidescroll/{ => Palace}/Palaces.cs | 3 +- .../RandomItemRoomSelectionStrategy.cs | 2 +- .../RandomWalkCoordinatePalaceGenerator.cs | 2 +- .../ReconstructedLoopyPalaceGenerator.cs | 2 +- .../ReconstructedPalaceGenerator.cs | 6 +- .../Sidescroll/{ => Palace}/Room.cs | 2 +- .../Sidescroll/{ => Palace}/RoomExitType.cs | 2 +- RandomizerCore/Sidescroll/Palace/RoomGroup.cs | 6 + .../Sidescroll/{ => Palace}/RoomPool.cs | 2 +- .../{ => Palace}/SegmentConnectionType.cs | 2 +- ...ntialPlacementCoordinatePalaceGenerator.cs | 4 +- .../ShapeFirstCoordinatePalaceGenerator.cs | 4 +- .../TowerCoordinatePalaceGenerator.cs | 2 +- .../{ => Palace}/VanillaPalaceGenerator.cs | 8 +- .../{ => Palace}/VanillaRoomPool.cs | 2 +- .../VanillaShufflePalaceGenerator.cs | 2 +- .../VanillaWeightedPalaceGenerator.cs | 2 +- RandomizerCore/Sidescroll/RoomGroup.cs | 6 - RandomizerCore/Sidescroll/SideviewEnums.cs | 33 + RandomizerCore/Sidescroll/Town/Town.cs | 195 +++++ RandomizerCore/Sidescroll/Town/TownMap.cs | 42 + RandomizerCore/Sidescroll/Town/TownType.cs | 57 ++ RandomizerCore/Sidescroll/Town/Towns.cs | 167 ++++ .../Sidescroll/Town/VanillaTownMap.cs | 50 ++ RandomizerCore/Text.cs | 38 +- RandomizerCore/Town.cs | 135 --- RandomizerCore/TownEnum.cs | 137 ++++ RandomizerCore/Towns.cs | 33 - RandomizerCore/Util.cs | 13 +- Statistics/Result.cs | 1 + Statistics/Statistics.cs | 1 + Tests/RoomSerializationTests.cs | 1 + Tests/Utils.cs | 1 + ValidateRooms/Program.cs | 1 + 65 files changed, 1594 insertions(+), 1235 deletions(-) rename RandomizerCore/Sidescroll/{ => Palace}/ByEntranceDirectionItemRoomSelectionStrategy.cs (96%) rename RandomizerCore/Sidescroll/{ => Palace}/ByShapeItemRoomSelectionStrategy.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ChaosPalaceGenerator.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/CoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ItemRoomSelectionStrategy.cs (90%) rename RandomizerCore/Sidescroll/{ => Palace}/Palace.cs (97%) rename RandomizerCore/Sidescroll/{ => Palace}/PalaceColors.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/PalaceGenerator.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/PalaceRooms.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/Palaces.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/RandomItemRoomSelectionStrategy.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/RandomWalkCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ReconstructedLoopyPalaceGenerator.cs (96%) rename RandomizerCore/Sidescroll/{ => Palace}/ReconstructedPalaceGenerator.cs (98%) rename RandomizerCore/Sidescroll/{ => Palace}/Room.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/RoomExitType.cs (98%) create mode 100644 RandomizerCore/Sidescroll/Palace/RoomGroup.cs rename RandomizerCore/Sidescroll/{ => Palace}/RoomPool.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/SegmentConnectionType.cs (95%) rename RandomizerCore/Sidescroll/{ => Palace}/SequentialPlacementCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/ShapeFirstCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/TowerCoordinatePalaceGenerator.cs (99%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaPalaceGenerator.cs (94%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaRoomPool.cs (97%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaShufflePalaceGenerator.cs (95%) rename RandomizerCore/Sidescroll/{ => Palace}/VanillaWeightedPalaceGenerator.cs (99%) delete mode 100644 RandomizerCore/Sidescroll/RoomGroup.cs create mode 100644 RandomizerCore/Sidescroll/Town/Town.cs create mode 100644 RandomizerCore/Sidescroll/Town/TownMap.cs create mode 100644 RandomizerCore/Sidescroll/Town/TownType.cs create mode 100644 RandomizerCore/Sidescroll/Town/Towns.cs create mode 100644 RandomizerCore/Sidescroll/Town/VanillaTownMap.cs delete mode 100644 RandomizerCore/Town.cs create mode 100644 RandomizerCore/TownEnum.cs delete mode 100644 RandomizerCore/Towns.cs diff --git a/CommandLine/Program.cs b/CommandLine/Program.cs index 3e3db67ee..de34047cc 100644 --- a/CommandLine/Program.cs +++ b/CommandLine/Program.cs @@ -6,6 +6,7 @@ using NLog; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.CommandLine; diff --git a/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs b/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs index 20e1c532c..5a0075651 100644 --- a/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs +++ b/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs @@ -16,6 +16,7 @@ using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; using CrossPlatformUI.Services; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace CrossPlatformUI.ViewModels; diff --git a/DumpRooms/Program.cs b/DumpRooms/Program.cs index 70c0788c0..e65401844 100644 --- a/DumpRooms/Program.cs +++ b/DumpRooms/Program.cs @@ -5,6 +5,7 @@ using System.Text.Json; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; ROM ROMData = new ROM(args[0], true); diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 2d562d666..905d529c1 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using Z2Randomizer.RandomizerCore.Overworld; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -76,16 +77,16 @@ public class CustomTexts ]; //Indexes in the hints list - private static readonly Dictionary TOWN_SIGN_INDEXES = new() + private static readonly Dictionary TOWN_SIGN_INDEXES = new() { - {Town.RAURU, 11}, - {Town.RUTO, 20}, - {Town.SARIA_NORTH, 29}, - {Town.MIDO_WEST, 41}, - {Town.NABOORU, 62}, - {Town.DARUNIA_WEST, 76}, - {Town.NEW_KASUTO, 86}, - {Town.OLD_KASUTO, 94}, + {TownType.RAURU, 11}, + {TownType.RUTO, 20}, + {TownType.SARIA, 29}, + {TownType.MIDO, 41}, + {TownType.NABOORU, 62}, + {TownType.DARUNIA, 76}, + {TownType.NEW_KASUTO, 86}, + {TownType.OLD_KASUTO, 94}, }; public const int WEST_TEXT_COUNT = 52; @@ -113,18 +114,16 @@ public class CustomTexts private const int HELPFUL_HINTS_COUNT = 4; //private static readonly Dictionary spellTextIndexes = { 15, 24, 35, 46, 70, 81, 93, 96 }; - private static readonly Dictionary townWizardTextIndexes = new() + private static readonly Dictionary townWizardTextIndexes = new() { - { Town.RAURU, 15 }, - { Town.RUTO, 24 }, - { Town.SARIA_NORTH, 35 }, - { Town.MIDO_WEST, 46 }, - { Town.MIDO_CHURCH, downstabGuyGotItemTextIndex }, - { Town.NABOORU, 70 }, - { Town.DARUNIA_ROOF, upstabGuyGotItemTextIndex }, - { Town.DARUNIA_WEST, 81 }, - { Town.NEW_KASUTO, 93 }, - { Town.OLD_KASUTO, 96 }, + { TownType.RAURU, 15 }, + { TownType.RUTO, 24 }, + { TownType.SARIA, 35 }, + { TownType.MIDO, 46 }, + { TownType.NABOORU, 70 }, + { TownType.DARUNIA, 81 }, + { TownType.NEW_KASUTO, 93 }, + { TownType.OLD_KASUTO, 96 }, }; private static readonly Dictionary wizardTextIndexesBySpell = new() @@ -387,18 +386,16 @@ public class CustomTexts "i wish$i had a$%%$like yours" ]; - public static readonly Dictionary WIZARD_SPELL_TEXTS_BY_TOWN = new() + public static readonly Dictionary WIZARD_SPELL_TEXTS_BY_TOWN = new() { - { Town.RAURU, [] }, - { Town.RUTO, ["A winner$is you"] }, - { Town.SARIA_NORTH, ["Water$you$doing?"] }, - { Town.MIDO_WEST, [] }, - { Town.MIDO_CHURCH, [] }, - { Town.NABOORU, ["Do not$drink the$fountain$water here$...$trust me."] }, - { Town.DARUNIA_ROOF, [] }, - { Town.DARUNIA_WEST, ["You saved$a kid$for this?", "Dont$forget to$get upstab"] }, - { Town.NEW_KASUTO, [] }, - { Town.OLD_KASUTO, ["Sorry$about the$moas"] } + { TownType.RAURU, [] }, + { TownType.RUTO, ["A winner$is you"] }, + { TownType.SARIA, ["Water$you$doing?"] }, + { TownType.MIDO, [] }, + { TownType.NABOORU, ["Do not$drink the$fountain$water here$...$trust me."] }, + { TownType.DARUNIA, ["You saved$a kid$for this?", "Dont$forget to$get upstab"] }, + { TownType.NEW_KASUTO, [] }, + { TownType.OLD_KASUTO, ["Sorry$about the$moas"] } }; public static readonly Dictionary WIZARD_SPELL_TEXTS_BY_COLLECTABLE = new() @@ -535,26 +532,29 @@ public static List GenerateTexts( GenerateSpellHints(locations, texts, props); } - Location baguLocation = locations.FirstOrDefault(i => i.ActualTown == Town.BAGU)!; + Location baguLocation = locations.FirstOrDefault(i => i.Town?.Type == TownType.BAGU)!; if(baguLocation == null) { throw new Exception("Bagu location not found while generating text"); } - Text? baguText = GenerateBaguText(baguLocation.Collectables[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); + Text? baguText = GenerateBaguText(baguLocation.GetAllCollectables()[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); if(baguText != null) { texts[baguTextIndex] = baguText; } - Location tableLocation = locations.FirstOrDefault(i => i.ActualTown == Town.SARIA_TABLE)!; - Text? mirrorText = GenerateMirrorTableText(tableLocation.Collectables[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); + Collectable tableColletable = (Collectable)locations.First(i => i.Town?.Type == TownType.SARIA) + .Town!.GetTownMap(VanillaTownMap.SARIA_TABLE)!.Collectable!; + Text? mirrorText = GenerateMirrorTableText(tableColletable, nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); if (mirrorText != null) { texts[gotMirrorTextIndex] = mirrorText; } - Location fountainLocation = locations.FirstOrDefault(i => i.ActualTown == Town.NABOORU_FOUNTAIN)!; - Text? fountainText = GenerateFountainText(fountainLocation.Collectables[0], nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); + + Collectable fountainCollectable = (Collectable)locations.First(i => i.Town?.Type == TownType.NABOORU) + .Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.Collectable!; + Text? fountainText = GenerateFountainText(fountainCollectable, nonhashRNG, props.UseCommunityText, props.IncludeQuestItemsInShuffle); if (fountainText != null) { texts[gotWaterTextIndex] = fountainText; @@ -571,17 +571,17 @@ public static List GenerateTexts( if (props.SpellItemHints && props.IncludeSwordTechsInShuffle) { - var downstabLoc = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.DOWNSTAB)); - var upstabLoc = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.UPSTAB)); + var downstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.DOWNSTAB)); + var upstabLoc = locations.First(i => i.GetAllCollectables().Contains(Collectable.UPSTAB)); Text hint; - if (downstabLoc == null) + if (props.StartWithDownstab) { hint = props.UseCommunityText ? new Text(STARTED_WITH_ITEM_TEXTS.Sample(nonhashRNG)!, Collectable.DOWNSTAB) : new Text(NON_COMMUNITY_STARTED_WITH_TEXT, Collectable.DOWNSTAB); } else { - hint = Text.GenerateHelpfulHint(locations.ToList(), downstabLoc, Collectable.DOWNSTAB, props.IncludeSpellsInShuffle); + hint = Text.GenerateHelpfulHint(downstabLoc, Collectable.DOWNSTAB, props.IncludeSpellsInShuffle); } texts[downstabClosedDoorTextIndex] = hint; if (upstabLoc == null) @@ -591,7 +591,7 @@ public static List GenerateTexts( } else { - hint = Text.GenerateHelpfulHint(locations.ToList(), upstabLoc, Collectable.UPSTAB, props.IncludeSpellsInShuffle); + hint = Text.GenerateHelpfulHint(upstabLoc, Collectable.UPSTAB, props.IncludeSpellsInShuffle); } texts[upstabClosedDoorTextIndex] = hint; if (props.SwapUpAndDownStab) @@ -683,9 +683,9 @@ private static Text GenerateBaguWoodsHint(Location bagu) private static void GenerateTownNameHints(List texts, IEnumerable locations, bool linkedFire) { - foreach (Location location in locations.Where(i => i.ActualTown != null && i.VanillaCollectable.IsSpell())) + foreach (Location location in locations.Where(i => i.Town?.GetWizard() != null)) { - texts[TOWN_SIGN_INDEXES[(Town)location.ActualTown!]] = GenerateTownSignHint(location.Collectables[0], linkedFire); + texts[TOWN_SIGN_INDEXES[(TownType)location.Town!.Type!]] = GenerateTownSignHint((Collectable)location.Town.GetWizard()!.Collectable!, linkedFire); } } private static Text GenerateTownSignHint(Collectable spell, bool linkedFire) @@ -701,12 +701,12 @@ private static Text GenerateTownSignHint(Collectable spell, bool linkedFire) private static Text GenerateWizardText(List texts, Random r, Location location, bool useCommunityText, bool z2MarioMode) { - if(location.ActualTown == null) + if(location.Town == null) { throw new Exception("Cannot generate text for Wizard outside of town"); } - Town town = (Town)location.ActualTown; - Collectable collectable = location.Collectables[0]; + TownType town = (TownType)location.Town.Type!; + Collectable collectable = (Collectable)location.Town.GetWizard()!.Collectable!; if (collectable.IsSpell() || collectable == Collectable.DOWNSTAB || collectable == Collectable.UPSTAB) @@ -874,7 +874,7 @@ private static List GenerateHelpfulHints(List hints, IEnumerable placedTowns = []; - List items = locations.SelectMany(i => i.Collectables).ToList(); + List items = locations.SelectMany(i => i.GetAllCollectables()).ToList(); items = items.Where(i => !i.IsInternalUse()).ToList(); if (props.SpellItemHints && props.IncludeSwordTechsInShuffle) @@ -935,7 +935,7 @@ private static List GenerateHelpfulHints(List hints, IEnumerable possibleHintLocations = locations.Where(i => i.Collectables.Contains(hintCollectable)).ToList(); + List possibleHintLocations = locations.Where(i => i.GetAllCollectables().Contains(hintCollectable)).ToList(); Location hintLocation; int town; @@ -955,9 +955,9 @@ private static List GenerateHelpfulHints(List hints, IEnumerable locations, List i.Collectables.Contains(Collectable.TROPHY))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.TROPHY))!; if(itemLocation != null) { - Text trophyHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.TROPHY, props.IncludeSpellsInShuffle); + Text trophyHint = Text.GenerateHelpfulHint(itemLocation, Collectable.TROPHY, props.IncludeSpellsInShuffle); hints[trophySpellHintIndex] = trophyHint; } - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.MEDICINE))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.MEDICINE))!; if (itemLocation != null) { - Text medHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.MEDICINE, props.IncludeSpellsInShuffle); + Text medHint = Text.GenerateHelpfulHint(itemLocation, Collectable.MEDICINE, props.IncludeSpellsInShuffle); hints[medicineSpellHintIndex] = medHint; } - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.CHILD))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.CHILD))!; if (itemLocation != null) { - Text kidHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.CHILD, props.IncludeSpellsInShuffle); + Text kidHint = Text.GenerateHelpfulHint(itemLocation, Collectable.CHILD, props.IncludeSpellsInShuffle); hints[childSpellHintIndex] = kidHint; } if (props.IncludeQuestItemsInShuffle) { - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.MIRROR))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.MIRROR))!; if (itemLocation != null) { - Text mirrorHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.MIRROR, props.IncludeSpellsInShuffle); + Text mirrorHint = Text.GenerateHelpfulHint(itemLocation, Collectable.MIRROR, props.IncludeSpellsInShuffle); hints[mirrorSpellHintIndex] = mirrorHint; } - itemLocation = locations.FirstOrDefault(i => i.Collectables.Contains(Collectable.WATER))!; + itemLocation = locations.FirstOrDefault(i => i.GetAllCollectables().Contains(Collectable.WATER))!; if (itemLocation != null) { - Text waterHint = Text.GenerateHelpfulHint(locations.ToList(), itemLocation, Collectable.WATER, props.IncludeSpellsInShuffle); + Text waterHint = Text.GenerateHelpfulHint(itemLocation, Collectable.WATER, props.IncludeSpellsInShuffle); hints[waterSpellHintIndex] = waterHint; } } @@ -1041,10 +1041,10 @@ private static void GenerateWizardTexts(List texts, IEnumerable { List vanillaText = new(texts); List usedWizardTexts = []; - List wizardLocations = itemLocs.Where(i => i.ActualTown != 0 && (i?.ActualTown?.IsWizardTown() ?? false)).ToList(); + List wizardLocations = itemLocs.Where(i => i.Town?.GetWizard() != null).ToList(); foreach (Location location in wizardLocations) { - if(location.ActualTown == null) + if(location.Town == null) { throw new Exception("Invalid town that is not a town"); } @@ -1055,7 +1055,7 @@ private static void GenerateWizardTexts(List texts, IEnumerable wizardHint = GenerateWizardText(vanillaText, r, location, useCommunityText, z2MarioMode); } while (useCommunityText && usedWizardTexts.Contains(wizardHint) && tries++ < 100); usedWizardTexts.Add(wizardHint); - texts[townWizardTextIndexes[(Town)location.ActualTown]] = wizardHint; + texts[townWizardTextIndexes[(TownType)location.Town.Type!]] = wizardHint; } } diff --git a/RandomizerCore/Enemy/Enemies.cs b/RandomizerCore/Enemy/Enemies.cs index 10865afea..949e8dafb 100644 --- a/RandomizerCore/Enemy/Enemies.cs +++ b/RandomizerCore/Enemy/Enemies.cs @@ -281,6 +281,50 @@ public enum EnemiesEast BOULDER_TOSSING_LIZALFOS = 0x1D, } +/// +/// IDs for town "enemies" +/// +public enum EnemiesTown +{ + FAIRY = 0x00, + CANDLE = 0x01, + DOOR = 0x02, + PURPLE_KEESE_03 = 0x03, + PURPLE_BOT = 0x04, + BIT = 0x05, + PURPLE_MOA = 0x06, + PURPLE_KEESE_07 = 0x07, + GOLD_GIRL = 0x08, + TOWNPERSON_09 = 0x09, + TALKING_ACHE = 0x0A, + TALKING_BIT = 0x0B, + INVISIBLE_DIALOG = 0x0C, + RIVER_MAN = 0x0D, + BLUE_LUMBERJACK_0E = 0x0E, + WIZARD = 0x0F, + KID = 0x10, + STATIONARY_BLUE_LADY_WITH_BOWL_11 = 0x11, + STATIONARY_RED_GRANNY = 0x12, + STATIONARY_RED_LUMBERJACK = 0x13, + STATIONARY_BLUE_LADY_WITH_BOWL_14 = 0x14, + STATIONARY_BLUE_GRANNY = 0x15, + STATIONARY_RED_LADY = 0x16, + LIFE_REFILL_LADY = 0x17, + MAGIC_REFILL_GRANNY = 0x18, + BLUE_LUMBERJACK_19 = 0x19, + RED_LADY_WITH_BOWL = 0x1A, + RED_GRANNY = 0x1B, + BLUE_LADY = 0x1C, + BLUE_LUMBERJACK_1D = 0x1D, + RED_GUY_1E = 0x1E, + RED_GUY_1F = 0x1F, + WALKING_LADY = 0x20, + MIRROR = 0x21, + SIGN = 0x22, + BUSY_TOWNSPERSON_GENERATOR = 0x23 + +} + /// /// IDs that are shared in palace 1-6 /// diff --git a/RandomizerCore/Enemy/PalaceEnemyShuffler.cs b/RandomizerCore/Enemy/PalaceEnemyShuffler.cs index 3985776c0..f9d0620be 100644 --- a/RandomizerCore/Enemy/PalaceEnemyShuffler.cs +++ b/RandomizerCore/Enemy/PalaceEnemyShuffler.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore.Enemy; diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index cba27e6e5..a3122e4d1 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -14,6 +14,9 @@ using Z2Randomizer.RandomizerCore.Enemy; using Z2Randomizer.RandomizerCore.Overworld; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; +using DynamicData; namespace Z2Randomizer.RandomizerCore; @@ -334,11 +337,29 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC AsmModule sideviewModule = new(); passedValidation = await FillPalaceRooms(sideviewModule); - assembler.Add(sideviewModule); } - //palaces.ForEach(i => Debug.WriteLine(i.GetLayoutDebug(PalaceStyle.TOWER, false))); + List palaceLocations = [westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, + mazeIsland.locationAtPalace4, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6]; + if(props.PalacesCanSwapContinent) + { + if(props.P7shuffle) + { + palaceLocations.Add(eastHyrule.locationAtGP); + palaceLocations.FisherYatesShuffle(r); + } + else + { + palaceLocations.FisherYatesShuffle(r); + palaceLocations.Add(eastHyrule.locationAtGP); + } + } + for(int i = 0; i < 7; i++) + { + palaceLocations[i].Palace = palaces[i]; + } + palaceLocations.ForEach(i => i.AccessRequirements = Requirements.DEFAULT_PALACE_REQUIREMENTS); ROMData.WriteKasutoJarAmount(props.NewKasutoBasementRequirement); ROMData.DoHackyFixes(); @@ -380,42 +401,42 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC List? customSpellOrder = props.IncludeSpellsInShuffle ? null : AllLocationsForReal() - .Where(l => l.ActualTown != null && Towns.STRICT_SPELL_LOCATIONS.Contains((Town)l.ActualTown)) - //This makes the assumption that is currently true that each "town" has exactly one item. - //If we later restructure towns to be omni-towns to get rid of fake towns, this will be untrue - .Select(l => l.Collectables[0]).ToList(); + .Where(l => l.Town?.GetWizard() != null) + .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); ROMData.CombineFireSpell(assembler, customSpellOrder, r); } - Dictionary spellMap = new() - { - { (Town)westHyrule.locationAtRauru.ActualTown!, westHyrule.locationAtRauru.Collectables[0] }, - { (Town)westHyrule.locationAtMido.ActualTown!, westHyrule.locationAtMido.Collectables[0] }, - { (Town)westHyrule.locationAtSariaNorth.ActualTown!, westHyrule.locationAtSariaNorth.Collectables[0] }, - { (Town)westHyrule.locationAtRuto.ActualTown!, westHyrule.locationAtRuto.Collectables[0] }, - { (Town)eastHyrule.townAtNabooru.ActualTown!, eastHyrule.townAtNabooru.Collectables[0] }, - { (Town)eastHyrule.townAtDarunia.ActualTown!, eastHyrule.townAtDarunia.Collectables[0] }, - { (Town)eastHyrule.townAtNewKasuto.ActualTown!, eastHyrule.townAtNewKasuto.Collectables[0] }, - { (Town)eastHyrule.townAtOldKasuto.ActualTown!, eastHyrule.townAtOldKasuto.Collectables[0] }, + /* + Dictionary spellMap = new() + { + { (TownType)westHyrule.rauru.ActualTown!, westHyrule.rauru.Collectables[0] }, + { (TownType)westHyrule.mido.ActualTown!, westHyrule.mido.Collectables[0] }, + { (TownType)westHyrule.sariaNorth.ActualTown!, westHyrule.sariaNorth.Collectables[0] }, + { (TownType)westHyrule.ruto.ActualTown!, westHyrule.ruto.Collectables[0] }, + { (TownType)eastHyrule.townAtNabooru.ActualTown!, eastHyrule.townAtNabooru.Collectables[0] }, + { (TownType)eastHyrule.townAtDarunia.ActualTown!, eastHyrule.townAtDarunia.Collectables[0] }, + { (TownType)eastHyrule.townAtNewKasuto.ActualTown!, eastHyrule.townAtNewKasuto.Collectables[0] }, + { (TownType)eastHyrule.townAtOldKasuto.ActualTown!, eastHyrule.townAtOldKasuto.Collectables[0] }, }; + */ - if (props.StartsWithCollectable(spellMap[Town.RUTO])) + if (props.StartsWithCollectable(westHyrule.ruto.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b14, 0x10); //Trophy } - if (props.StartsWithCollectable(spellMap[Town.SARIA_NORTH])) + if (props.StartsWithCollectable(westHyrule.sariaNorth.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b15, 0x01); //Mirror } - if (props.StartsWithCollectable(spellMap[Town.MIDO_WEST])) + if (props.StartsWithCollectable(westHyrule.mido.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b16, 0x40); //Medicine } - if (props.StartsWithCollectable(spellMap[Town.NABOORU])) + if (props.StartsWithCollectable(eastHyrule.nabooru.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b17, 0x01); //Water } - if (props.StartsWithCollectable(spellMap[Town.DARUNIA_WEST])) + if (props.StartsWithCollectable(eastHyrule.darunia.Town?.GetWizard()?.Collectable)) { ROMData.Put(0x17b18, 0x20); //Child } @@ -674,75 +695,67 @@ private static void SanitizeHashCharacters(byte[] z2Hash) // hand out a duplicate. private void ReplaceDifficultyStartingItemsInWorld(Random r) { - List minorItems = [ - Collectable.BLUE_JAR, Collectable.RED_JAR, Collectable.SMALL_BAG, - Collectable.MEDIUM_BAG, Collectable.LARGE_BAG, Collectable.XL_BAG, - Collectable.ONEUP, Collectable.KEY - ]; Collectable[] difficultyStartingItems = [Collectable.CANDLE, Collectable.CROSS]; - + Collectable[] minorItems = Enum.GetValues().Where(i => i.IsMinorItem()).ToArray(); foreach (Collectable item in difficultyStartingItems) { if (!props.StartsWithCollectable(item)) { continue; } + Collectable replacementItem = minorItems.Sample(r); foreach (Location location in itemLocs) { - for (int i = 0; i < location.Collectables.Count; i++) + if(location.ReplaceCollectable(item, replacementItem)) { - if (location.Collectables[i] == item) - { - location.Collectables[i] = minorItems.Sample(r); - } + return; } } } } - /* - Text Notes: - - Community Text Changes - ---------------------- - Shield Spell 15 43 - Cannot Help 16 35 - Jump Spell 24 34 - Life Spell 35 37 - You know..? 37 42 - Fairy 46 37 - Downstab 47 38 - Bagu 48 44 - Fire 70 43 - You know 71 34 - Reflect 81 37 - Upstab 82 32 - Spell 93 25 - Thunder 96 36 - */ + //Shuffle the palace itself, not just the metadata + private void ShufflePalaces() + { + if (!props.PalacesCanSwapContinent) return; + List palaces = [westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, mazeIsland.locationAtPalace4, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6]; + + if (props.P7shuffle) + { + palaces.Add(eastHyrule.locationAtGP); + } + + for (int i = palaces.Count - 1; i > 0; i--) + { + int swap = r.Next(i + 1); + (palaces[i].Palace, palaces[swap].Palace) = (palaces[swap].Palace, palaces[i].Palace); + } + + } private void ShuffleItems() { - List shufflableItems = [ - Collectable.CANDLE, Collectable.GLOVE, Collectable.RAFT, Collectable.BOOTS, - Collectable.FLUTE, Collectable.CROSS, Collectable.HEART_CONTAINER, Collectable.HEART_CONTAINER, - Collectable.MAGIC_CONTAINER, Collectable.MEDICINE, Collectable.TROPHY, Collectable.HEART_CONTAINER, - Collectable.HEART_CONTAINER, Collectable.MAGIC_CONTAINER, Collectable.MAGIC_KEY, Collectable.MAGIC_CONTAINER, - Collectable.HAMMER, Collectable.CHILD, Collectable.MAGIC_CONTAINER]; + List shufflableItems = []; + List minorItems = [Collectable.BLUE_JAR, Collectable.RED_JAR, Collectable.SMALL_BAG, Collectable.MEDIUM_BAG, Collectable.LARGE_BAG, Collectable.XL_BAG, Collectable.ONEUP, Collectable.KEY]; if (props.PbagItemShuffle) { - westHyrule.pbagCave.Collectables = [(Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE)]; - eastHyrule.pbagCave1.Collectables = [(Collectable)ROMData.GetByte(RomMap.EAST_PBAG_CAVE1_COLLECTABLE)]; - eastHyrule.pbagCave2.Collectables = [(Collectable)ROMData.GetByte(RomMap.EAST_PBAG_CAVE2_COLLECTABLE)]; - shufflableItems.Add(westHyrule.pbagCave.Collectables[0]); - shufflableItems.Add(eastHyrule.pbagCave1.Collectables[0]); - shufflableItems.Add(eastHyrule.pbagCave2.Collectables[0]); + Collectable collectable = (Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE); + westHyrule.pbagCave.SetCollectables([collectable]); + shufflableItems.Add(collectable); + + collectable = (Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE); + eastHyrule.pbagCave1.SetCollectables([collectable]); + shufflableItems.Add(collectable); + + collectable = (Collectable)ROMData.GetByte(RomMap.WEST_PBAG_CAVE_COLLECTABLE); + eastHyrule.pbagCave2.SetCollectables([collectable]); + shufflableItems.Add(collectable); } else { - westHyrule.pbagCave.Collectables = [Collectable.LARGE_BAG]; - eastHyrule.pbagCave1.Collectables = [Collectable.LARGE_BAG]; - eastHyrule.pbagCave2.Collectables = [Collectable.XL_BAG]; + westHyrule.pbagCave.SetCollectables([Collectable.LARGE_BAG]); + eastHyrule.pbagCave1.SetCollectables([Collectable.LARGE_BAG]); + eastHyrule.pbagCave2.SetCollectables([Collectable.XL_BAG]); } if(props.IncludeSpellsInShuffle) @@ -860,25 +873,26 @@ private void ShuffleItems() } else if(!props.IncludeSpellsInShuffle) { - Collectable townCollectable; - townCollectable = westHyrule.AllLocations.First(i => i.ActualTown == Town.RUTO).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + Collectable wizardCollectable; + //wizardCollectable = westHyrule.AllLocations.First(i => i.ActualTown == TownType.RUTO).Collectables[0]; + wizardCollectable = (Collectable)westHyrule.ruto.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[10] == Collectable.TROPHY); shufflableItems[10] = minorItems[r.Next(minorItems.Count)]; ItemGet[Collectable.TROPHY] = true; } - townCollectable = westHyrule.AllLocations.First(i => i.ActualTown == Town.MIDO_WEST).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)westHyrule.mido.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[9] == Collectable.MEDICINE); shufflableItems[9] = minorItems[r.Next(minorItems.Count)]; ItemGet[Collectable.MEDICINE] = true; } - townCollectable = eastHyrule.AllLocations.First(i => i.ActualTown == Town.DARUNIA_WEST).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)eastHyrule.darunia.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[17] == Collectable.CHILD); shufflableItems[17] = minorItems[r.Next(minorItems.Count)]; @@ -887,16 +901,16 @@ private void ShuffleItems() if (props.IncludeQuestItemsInShuffle) { - townCollectable = westHyrule.AllLocations.First(i => i.ActualTown == Town.SARIA_NORTH).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)westHyrule.sariaNorth.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[mirrorIndex] == Collectable.MIRROR); shufflableItems[mirrorIndex] = minorItems[r.Next(minorItems.Count)]; ItemGet[Collectable.MIRROR] = true; } - townCollectable = eastHyrule.AllLocations.First(i => i.ActualTown == Town.NABOORU).Collectables[0]; - if (!townCollectable.IsMinorItem() && ItemGet[townCollectable]) + wizardCollectable = (Collectable)eastHyrule.nabooru.Town!.GetWizard()!.Collectable!; + if (!wizardCollectable.IsMinorItem() && ItemGet[wizardCollectable]) { Debug.Assert(shufflableItems[waterindex] == Collectable.WATER); shufflableItems[waterindex] = minorItems[r.Next(minorItems.Count)]; @@ -944,7 +958,11 @@ private void ShuffleItems() } } - if(props.IncludeSwordTechsInShuffle) + itemLocs.SelectMany(i => i.Town?.TownMaps ?? []) + .First(i => i.Map == (int)VanillaTownMap.MIDO_TRAINER).CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + itemLocs.SelectMany(i => i.Town?.TownMaps ?? []) + .First(i => i.Map == (int)VanillaTownMap.DARUNIA_TRAINER).CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + if (props.IncludeSwordTechsInShuffle) { if (props.StartWithDownstab) { @@ -1061,13 +1079,13 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location palaceLocation in itemLocs.Where(i => i.PalaceNumber != null)) + foreach (Location palaceLocation in itemLocs.Where(i => i.Palace != null)) { shufflableItemLocations.Add(palaceLocation); - Collectable vanillaCollectable = Palace.GetVanillaCollectable(palaceLocation.PalaceNumber); + Collectable vanillaCollectable = Palace.GetVanillaCollectable(palaceLocation.Palace!.Number); itemsToActuallyShuffle.Add(shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)); - for (int i = 1; i < props.PalaceItemRoomCounts[(int)palaceLocation.PalaceNumber! - 1]; i++) + for (int i = 1; i < props.PalaceItemRoomCounts[(int)palaceLocation.Palace.Number! - 1]; i++) { itemsToActuallyShuffle.Add(minorItems.Sample(r)); } @@ -1075,22 +1093,23 @@ private void ShuffleItems() duplicateItemPlacementCandidates.AddRange(shufflableItemLocations); DoShuffle(itemsToActuallyShuffle, shufflableItemLocations); } - else + else //no shuffle palace items { - foreach (Location palaceLocation in itemLocs.Where(i => i.PalaceNumber != null && i.PalaceNumber < 7)) + foreach (Location palaceLocation in itemLocs.Where(i => i.Palace != null && i.Palace.Number < 7)) { - Collectable vanillaCollectable = Palace.GetVanillaCollectable(palaceLocation.PalaceNumber); - palaceLocation.Collectables = [shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)]; - palaces[(int)palaceLocation.PalaceNumber! - 1].ItemRooms.Sample(r)!.Collectable = vanillaCollectable; - for (int i = 0; i < props.PalaceItemRoomCounts[(int)palaceLocation.PalaceNumber! - 1]; i++) + int palaceNumber = palaceLocation.Palace!.Number; + Collectable vanillaCollectable = Palace.GetVanillaCollectable(palaceNumber); + List collectables = []; + collectables = [shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)]; + for (int i = 0; i < props.PalaceItemRoomCounts[(int)palaceNumber! - 1]; i++) { - if (palaces[(int)palaceLocation.PalaceNumber! - 1].ItemRooms[i].Collectable == null) + if (palaces[(int)palaceNumber! - 1].ItemRooms[i].Collectable == null) { Collectable smallItem = minorItems.Sample(r); - palaceLocation.Collectables.Add(smallItem); - palaces[(int)palaceLocation.PalaceNumber! - 1].ItemRooms[i].Collectable = smallItem; + collectables.Add(smallItem); } } + palaceLocation.SetCollectables(collectables); } } @@ -1098,90 +1117,87 @@ private void ShuffleItems() { itemsToActuallyShuffle = []; shufflableItemLocations = []; - foreach (Location nonPalaceLocation in itemLocs.Where(i => i.PalaceNumber == null)) + foreach (Location nonPalaceLocation in itemLocs.Where(i => i.Palace == null)) { shufflableItemLocations.Add(nonPalaceLocation); - itemsToActuallyShuffle.Add(shufflableItems.Contains(nonPalaceLocation.VanillaCollectable) - ? nonPalaceLocation.VanillaCollectable : minorItems.Sample(r)); + foreach(Collectable collectable in nonPalaceLocation.GetAllCollectables()) + { + itemsToActuallyShuffle.Add(shufflableItems.Contains(collectable) ? collectable : minorItems.Sample(r)); + } } duplicateItemPlacementCandidates.AddRange(shufflableItemLocations); DoShuffle(itemsToActuallyShuffle, shufflableItemLocations); } else { - foreach (Location nonPalaceLocation in itemLocs.Where(i => i.PalaceNumber == null)) + foreach (Location nonPalaceLocation in itemLocs.Where(i => i.Palace == null)) { - Collectable vanillaCollectable = nonPalaceLocation.VanillaCollectable; - nonPalaceLocation.Collectables = [shufflableItems.Contains(vanillaCollectable) ? vanillaCollectable : minorItems.Sample(r)]; + List collectables = nonPalaceLocation.GetAllCollectables(); + for(int i = 0; i < collectables.Count; i++) + { + collectables[i] = shufflableItems.Contains(collectables[i]) ? collectables[i] : minorItems.Sample(r); + } + nonPalaceLocation.SetCollectables(collectables); } } } //Assigning unshuffled locations can make the number of containers wrong, so re-adjust them - List heartContainerLocations = itemLocs.Where(i => i.Collectables.Contains(Collectable.HEART_CONTAINER)).ToList(); - int heartContainerCount = itemLocs.SelectMany(i => i.Collectables).Count(i => i == Collectable.HEART_CONTAINER); + List heartContainerLocations = itemLocs.Where(i => i.GetAllCollectables().Contains(Collectable.HEART_CONTAINER)).ToList(); + int heartContainerCount = heartContainerLocations.SelectMany(i => i.GetAllCollectables()).Count(i => i == Collectable.HEART_CONTAINER); while(heartContainerCount > heartContainersInItemPool) { Location location = heartContainerLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index] == Collectable.HEART_CONTAINER) + Collectable minorItem = minorItems.Sample(r); + if(!location.ReplaceCollectable(Collectable.HEART_CONTAINER, minorItem)) { - location.Collectables[index] = minorItems.Sample(r); - heartContainerCount--; - if (!location.Collectables.Any(i => i == Collectable.HEART_CONTAINER)) - { - heartContainerLocations.Remove(location); - } + heartContainerLocations.Remove(location); } } - while(heartContainerCount < heartContainersInItemPool) - { - List minorItemLocations = itemLocs.Where(i => i.Collectables.Any(j => j.IsMinorItem())).ToList(); + List minorItemLocations = itemLocs.Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); + while (heartContainerCount < heartContainersInItemPool) + { Location location = minorItemLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index].IsMinorItem()) + Collectable[] locationMinorItems = location.GetAllCollectables().Where(i => i.IsMinorItem()).ToArray(); + if(locationMinorItems.Length == 0) { - location.Collectables[index] = Collectable.HEART_CONTAINER; - heartContainerCount++; - if (!location.Collectables.Any(i => i.IsMinorItem())) - { - minorItemLocations.Remove(location); - } + minorItemLocations.Remove(location); + continue; + } + if (!location.ReplaceCollectable(locationMinorItems.Sample(r), Collectable.HEART_CONTAINER)) + { + heartContainerLocations.Remove(location); } } - List magicContainerLocations = itemLocs.Where(i => i.Collectables.Contains(Collectable.MAGIC_CONTAINER)).ToList(); - int magicContainerCount = itemLocs.SelectMany(i => i.Collectables).Count(i => i == Collectable.MAGIC_CONTAINER); + + List magicContainerLocations = itemLocs.Where(i => i.GetAllCollectables().Contains(Collectable.MAGIC_CONTAINER)).ToList(); + int magicContainerCount = magicContainerLocations.SelectMany(i => i.GetAllCollectables()).Count(i => i == Collectable.MAGIC_CONTAINER); while (magicContainerCount > magicContainersInItemPool) { Location location = magicContainerLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index] == Collectable.MAGIC_CONTAINER) + Collectable minorItem = minorItems.Sample(r); + if (!location.ReplaceCollectable(Collectable.MAGIC_CONTAINER, minorItem)) { - location.Collectables[index] = minorItems.Sample(r); - magicContainerCount--; - if (!location.Collectables.Any(i => i == Collectable.MAGIC_CONTAINER)) - { - magicContainerLocations.Remove(location); - } + magicContainerLocations.Remove(location); } } + minorItemLocations = itemLocs.Where(i => i.GetAllCollectables().Any(j => j.IsMinorItem())).ToList(); + while (magicContainerCount < magicContainersInItemPool) { - List minorItemLocations = itemLocs.Where(i => i.Collectables.Any(j => j.IsMinorItem())).ToList(); - Location location = minorItemLocations.Sample(r)!; - int index = r.Next(location.Collectables.Count); - if (location.Collectables[index].IsMinorItem()) + Collectable[] locationMinorItems = location.GetAllCollectables().Where(i => i.IsMinorItem()).ToArray(); + if (locationMinorItems.Length == 0) { - location.Collectables[index] = Collectable.MAGIC_CONTAINER; - magicContainerCount++; - if (!location.Collectables.Any(i => i.IsMinorItem())) - { - minorItemLocations.Remove(location); - } + minorItemLocations.Remove(location); + continue; + } + if (!location.ReplaceCollectable(locationMinorItems.Sample(r), Collectable.MAGIC_CONTAINER)) + { + magicContainerLocations.Remove(location); } } @@ -1204,8 +1220,8 @@ private void ShuffleItems() importantItemsToDuplicate = importantItemsToDuplicate.Where(shufflableItems.Contains).ToList(); - List minorItemLocations = duplicateItemPlacementCandidates.Where(i => i.Collectables.Any(c => c.IsMinorItem())).ToList(); - int replaceableMinorItemCount = duplicateItemPlacementCandidates.Sum(l => l.Collectables.Count(c => c.IsMinorItem())); + minorItemLocations = duplicateItemPlacementCandidates.Where(i => i.GetAllCollectables().Any(c => c.IsMinorItem())).ToList(); + int replaceableMinorItemCount = duplicateItemPlacementCandidates.Sum(l => l.GetAllCollectables().Count(c => c.IsMinorItem())); importantItemsToDuplicate = importantItemsToDuplicate.GetRange(0, int.Min(replaceableMinorItemCount, importantItemsToDuplicate.Count)); importantItemsToDuplicate.FisherYatesShuffle(r); @@ -1214,13 +1230,16 @@ private void ShuffleItems() for (int itemIndex = 0; itemIndex < importantItemsToDuplicate.Count; itemIndex++) { Location minorItemLocation = minorItemLocations.Sample(r)!; - minorItemLocation.Collectables.FisherYatesShuffle(r); - for(int collectableIndex = 0; collectableIndex < minorItemLocation.Collectables.Count; collectableIndex++) + List locationCollectables = minorItemLocation.GetAllCollectables(); + List collectableIndexes = Enumerable.Range(0, minorItemLocation.GetAllCollectables().Count()).ToList(); + collectableIndexes.FisherYatesShuffle(r); + foreach(int collectableIndex in collectableIndexes) { - if(minorItemLocation.Collectables[collectableIndex].IsMinorItem()) + if(locationCollectables[collectableIndex].IsMinorItem()) { - minorItemLocation.Collectables[collectableIndex] = importantItemsToDuplicate[itemIndex]; - if(!minorItemLocation.Collectables.Any(c => c.IsMinorItem())) + locationCollectables[collectableIndex] = importantItemsToDuplicate[itemIndex]; + minorItemLocation.SetCollectables(locationCollectables); + if(!locationCollectables.Any(c => c.IsMinorItem())) { minorItemLocations.Remove(minorItemLocation); } @@ -1233,48 +1252,24 @@ private void ShuffleItems() private void DoShuffle(List itemsToShuffle, List itemShuffleLocations) { - int itemShuffleLocationsCount = itemShuffleLocations.Count; - if(itemShuffleLocations.Any(i => i.PalaceNumber != null)) - { - itemShuffleLocationsCount = itemShuffleLocationsCount - - itemShuffleLocations.Count(i => i.PalaceNumber != null) - + props.PalaceItemRoomCounts.Sum(i => i); - //- props.PalaceItemRoomCounts.Where(i => i == 0).Count(); - } - if (itemsToShuffle.Count != itemShuffleLocationsCount) + int possibleItemPlacements = itemShuffleLocations.Sum(i => i.GetAllCollectables().Count); + + if (itemsToShuffle.Count != possibleItemPlacements) { throw new Exception("Item locations must match number of items"); } itemsToShuffle.FisherYatesShuffle(r); - //Clear all locations, then set them to the newly shuffled items - itemShuffleLocations.ForEach(i => i.Collectables.Clear()); - using var itemLocsIterator = itemShuffleLocations.GetEnumerator(); - Location? location = null; - int subIndex = 0; - foreach (Collectable item in itemsToShuffle) + int itemIndex = 0; + foreach(Location location in itemShuffleLocations) { - if (location?.PalaceNumber == null) - { - if (!itemLocsIterator.MoveNext()) { throw new InvalidOperationException("Ran out of item locations."); } - location = itemLocsIterator.Current; - } - while (location.PalaceNumber is int palaceNum && ++subIndex > props.PalaceItemRoomCounts[palaceNum - 1]) - { - subIndex = 0; - if (!itemLocsIterator.MoveNext()) { throw new InvalidOperationException("Ran out of item locations."); } - location = itemLocsIterator.Current; - } - - location.Collectables.Add(item); - if (location.PalaceNumber is int palaceNumInIf) - { - palaces[palaceNumInIf - 1].ItemRooms[subIndex - 1].Collectable = item; - } + int itemCount = location.GetAllCollectables().Count; + List collectables = itemsToShuffle.GetRange(itemIndex, itemCount); + itemIndex += itemCount; + location.SetCollectables(collectables); } - Debug.Assert(!itemLocsIterator.MoveNext(), "All item locations were not used. This should not happen."); } private async Task FillPalaceRooms(AsmModule sideviewModule) @@ -1621,7 +1616,7 @@ private int UpdateItemGets(RandomizerProperties props) List requireables; accessibleMagicContainers = props.StartMagicContainers; accessibleHeartContainers = props.StartHearts; - Location newKasuto = eastHyrule.AllLocations.First(i => i.ActualTown == Town.NEW_KASUTO); + Location newKasuto = eastHyrule.newKasuto; List gottenItems = [], lastIterationGottenItems = []; List locations = AllLocationsForReal().ToList(); List heartContainerGetLocations = [], magicContainerGetLocations = []; @@ -1636,39 +1631,17 @@ private int UpdateItemGets(RandomizerProperties props) gottenItems = []; foreach (Location location in AllLocationsForReal()) { - foreach (Collectable collectable in location.Collectables) - { - if (collectable.IsInternalUse()) - { - continue; - } - bool canGet; - //Location is a palace - if (location.PalaceNumber != null && location.PalaceNumber < 7) - { - Debug.Assert(location.CollectableRequirements == World.DEFAULT_PALACE_REQUIREMENTS); - Palace palace = palaces[(int)location.PalaceNumber - 1]; - canGet = CanGet(location) - //All palaces inherently require fairy spell or magic key - && location.CollectableRequirements.AreSatisfiedBy(requireables) - && palace.GetGettableItems(requireables).Contains(collectable); - } - else - { - canGet = CanGet(location) && location.CollectableRequirements.AreSatisfiedBy(requireables); - } + List gettableItems = location.GetGettableItems(requireables); - ItemGet[collectable] = canGet; - if (canGet) - { - gottenItems.Add(collectable); - } + foreach(Collectable item in gettableItems) + { + gottenItems.Add(item); - if (canGet && collectable == Collectable.HEART_CONTAINER && !heartContainerGetLocations.Contains(location)) + if (item == Collectable.HEART_CONTAINER && !heartContainerGetLocations.Contains(location)) { heartContainerGetLocations.Add(location); } - if (canGet && collectable == Collectable.MAGIC_CONTAINER && !magicContainerGetLocations.Contains(location)) + if (item == Collectable.MAGIC_CONTAINER && !magicContainerGetLocations.Contains(location)) { magicContainerGetLocations.Add(location); } @@ -1763,7 +1736,6 @@ private async Task ProcessOverworld(Func progress, CancellationTok worlds.Add(deathMountain); worlds.Add(eastHyrule); worlds.Add(mazeIsland); - ResetTowns(); if (props.ContinentConnections == ContinentConnectionType.NORMAL) { @@ -2053,7 +2025,6 @@ private async Task ProcessOverworld(Func progress, CancellationTok timeSpentBuildingMI += (int)DateTime.Now.Subtract(timestamp).TotalMilliseconds; worlds.Where(i => i.biome == Biome.VANILLA_SHUFFLE).ToList().ForEach(i => i.DisableDisallowedPassthroughs()); - worlds.ForEach(i => i.SynchronizeLinkedLocations()); if (ct.IsCancellationRequested) { return; } UpdateProgress(progress, 6); @@ -2081,11 +2052,8 @@ private async Task ProcessOverworld(Func progress, CancellationTok location.Reachable = false; } - eastHyrule.spellTower.Reachable = false; - westHyrule.ResetVisitabilityState(); - eastHyrule.ResetVisitabilityState(); - mazeIsland.ResetVisitabilityState(); - deathMountain.ResetVisitabilityState(); + worlds.ForEach(i => i.ResetVisitabilityState()); + worlds.ForEach(i => i.ResetCollectables(props)); //There was a spooky extra call to LoadItemLocs that used to be here that shouldn't be needed, but be aware. westHyrule.SetStart(); @@ -2208,132 +2176,86 @@ private bool AllContinentsAreConnected((Location, Location)[] connections) return continentsFound.Count == 4; } - private void ResetTowns() - { - westHyrule.locationAtRauru.ActualTown = Town.RAURU; - westHyrule.locationAtRuto.ActualTown = Town.RUTO; - westHyrule.locationAtSariaNorth.ActualTown = Town.SARIA_NORTH; - westHyrule.locationAtSariaSouth.ActualTown = Town.SARIA_SOUTH; - westHyrule.locationAtMido.ActualTown = Town.MIDO_WEST; - eastHyrule.townAtNabooru.ActualTown = Town.NABOORU; - eastHyrule.townAtDarunia.ActualTown = Town.DARUNIA_WEST; - eastHyrule.townAtNewKasuto.ActualTown = Town.NEW_KASUTO; - eastHyrule.townAtOldKasuto.ActualTown = Town.OLD_KASUTO; - } - - private void ShufflePalaces() - { - if (!props.PalacesCanSwapContinent) return; - List pals = [westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, mazeIsland.locationAtPalace4, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6]; - - if (props.P7shuffle) - { - pals.Add(eastHyrule.locationAtGP); - } - - for (int i = pals.Count - 1; i > 0; i--) - { - int swap = r.Next(i + 1); - Util.Swap(pals[i], pals[swap]); - } - - } - //ItemLocs is specifically only those locations that contain shufflable items //If you're looking for a global reference for which items are where... too bad it doesn't exist :( private List LoadItemLocs(int[] itemsPerPalaces) { List GetPalacesWithItems(IEnumerable palaces) => - palaces.Where(p => p.PalaceNumber is int n && n != 7 && props.PalaceItemRoomCounts[n - 1] != 0).ToList(); + palaces.Where(p => p.Palace?.Number is int n && n != 7 && props.PalaceItemRoomCounts[n - 1] != 0).ToList(); // Listed somewhat in order of expected progression. This way we can // use the indexes of two locations to determine approx distance. itemLocs = new List(30); + //palaces itemLocs = GetPalacesWithItems([ westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, - westHyrule.locationAtPalace3 + westHyrule.locationAtPalace3, + mazeIsland.locationAtPalace4, + eastHyrule.locationAtPalace5, + eastHyrule.locationAtPalace6, + eastHyrule.locationAtGP ]); + + //always live locations itemLocs.AddRange([ westHyrule.grassTile, westHyrule.heartContainerCave, westHyrule.magicContainerCave, westHyrule.medicineCave, westHyrule.trophyCave, - ]); - if (props.PbagItemShuffle) - { - itemLocs.Add(westHyrule.pbagCave); - } - if (props.IncludeQuestItemsInShuffle) - { - itemLocs.Add(westHyrule.bagu); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(westHyrule.locationAtRauru); - itemLocs.Add(westHyrule.locationAtRuto); - itemLocs.Add(westHyrule.locationAtSariaNorth); - } - if (props.IncludeQuestItemsInShuffle) - { - itemLocs.Add(westHyrule.mirrorTable); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(westHyrule.locationAtMido); - } - if (props.IncludeSwordTechsInShuffle) - { - itemLocs.Add(westHyrule.midoChurch); - } - - itemLocs.AddRange([ deathMountain.specRock, deathMountain.hammerCave, - ]); - - itemLocs.AddRange(GetPalacesWithItems([ - eastHyrule.locationAtPalace5, - eastHyrule.locationAtPalace6, - eastHyrule.locationAtGP - ])); - itemLocs.AddRange([ eastHyrule.waterTile, eastHyrule.desertTile, + mazeIsland.childDrop, + mazeIsland.magicContainerDrop ]); + + //pbag shuffle if (props.PbagItemShuffle) { + itemLocs.Add(westHyrule.pbagCave); itemLocs.Add(eastHyrule.pbagCave1); itemLocs.Add(eastHyrule.pbagCave2); } + + + //Quest item shuffle if (props.IncludeQuestItemsInShuffle) { - itemLocs.Add(eastHyrule.fountain); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(eastHyrule.townAtNabooru); - itemLocs.Add(eastHyrule.townAtDarunia); - } - if (props.IncludeSwordTechsInShuffle) - { - itemLocs.Add(eastHyrule.daruniaRoof); - } - if (props.IncludeSpellsInShuffle) - { - itemLocs.Add(eastHyrule.townAtOldKasuto); - itemLocs.Add(eastHyrule.townAtNewKasuto); + itemLocs.Add(westHyrule.bagu); } - itemLocs.Add(eastHyrule.newKasutoBasement); - itemLocs.Add(eastHyrule.spellTower); + westHyrule.bagu.Town!.GetTownMap(VanillaTownMap.BAGU_INDOORS)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + westHyrule.sariaNorth.Town!.GetTownMap(VanillaTownMap.BAGU_INDOORS)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + eastHyrule.nabooru.Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; - itemLocs.AddRange(GetPalacesWithItems([mazeIsland.locationAtPalace4])); - itemLocs.AddRange([ - mazeIsland.childDrop, - mazeIsland.magicContainerDrop - ]); + + itemLocs.Add(westHyrule.rauru); + itemLocs.Add(westHyrule.ruto); + itemLocs.Add(westHyrule.sariaNorth); + itemLocs.Add(westHyrule.mido); + itemLocs.Add(eastHyrule.nabooru); + itemLocs.Add(eastHyrule.darunia); + itemLocs.Add(eastHyrule.oldKasuto); + itemLocs.Add(eastHyrule.newKasuto); + + westHyrule.rauru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + westHyrule.ruto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + westHyrule.sariaNorth.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + westHyrule.mido.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.nabooru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.darunia.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.newKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + eastHyrule.oldKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + //stab shuffle + westHyrule.mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + eastHyrule.darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + + //Remove locations that don't have any shufflable items + itemLocs.RemoveAll(i => i.GetShufflableCollectables().Count == 0); return itemLocs; } @@ -2353,8 +2275,8 @@ private void ShuffleSpells() } } - List unallocatedTowns = new List { Town.RAURU, Town.RUTO, Town.SARIA_NORTH, Town.MIDO_WEST, - Town.NABOORU, Town.DARUNIA_WEST, Town.NEW_KASUTO, Town.OLD_KASUTO }; + List unallocatedTowns = new List { westHyrule.rauru, westHyrule.ruto, westHyrule.sariaNorth, westHyrule.mido, + eastHyrule.nabooru, eastHyrule.darunia, eastHyrule.newKasuto, eastHyrule.oldKasuto }; var filteredToJustSpells = Enum.GetValues(typeof(Collectable)).Cast().Where(i => i.IsSpell()); foreach (Collectable spell in filteredToJustSpells) @@ -2366,17 +2288,16 @@ private void ShuffleSpells() { continue; } - Town town = unallocatedTowns[r.Next(unallocatedTowns.Count)]; - Location townLocation = GetTownLocation(town); - townLocation.Collectables = [spell]; - unallocatedTowns.Remove(town); + Location townLocation = unallocatedTowns.Sample(r)!; + townLocation.Town!.GetWizard()!.Collectable = spell; + unallocatedTowns.Remove(townLocation); } } private void SwapUpAndDownstab() { - eastHyrule.daruniaRoof.Collectables = [Collectable.DOWNSTAB]; - westHyrule.midoChurch.Collectables = [Collectable.UPSTAB]; + eastHyrule.darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.Collectable = Collectable.DOWNSTAB; + westHyrule.mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.Collectable = Collectable.UPSTAB; } //Updated to use fisher-yates. Eventually i'll catch all of these. N is small enough here it REALLY makes a difference @@ -2671,13 +2592,13 @@ private void UpdateRom() palace.WriteConnections(ROMData); } - ROMData.Put(0x1CD3A, (byte)palGraphics[(int)westHyrule.locationAtPalace1.PalaceNumber!]); - ROMData.Put(0x1CD3B, (byte)palGraphics[(int)westHyrule.locationAtPalace2.PalaceNumber!]); - ROMData.Put(0x1CD3C, (byte)palGraphics[(int)westHyrule.locationAtPalace3.PalaceNumber!]); - ROMData.Put(0x1CD46, (byte)palGraphics[(int)mazeIsland.locationAtPalace4.PalaceNumber!]); - ROMData.Put(0x1CD42, (byte)palGraphics[(int)eastHyrule.locationAtPalace5.PalaceNumber!]); - ROMData.Put(0x1CD43, (byte)palGraphics[(int)eastHyrule.locationAtPalace6.PalaceNumber!]); - ROMData.Put(0x1CD44, (byte)palGraphics[(int)eastHyrule.locationAtGP.PalaceNumber!]); + ROMData.Put(0x1CD3A, (byte)palGraphics[(int)westHyrule.locationAtPalace1.Palace!.Number!]); + ROMData.Put(0x1CD3B, (byte)palGraphics[(int)westHyrule.locationAtPalace2.Palace!.Number!]); + ROMData.Put(0x1CD3C, (byte)palGraphics[(int)westHyrule.locationAtPalace3.Palace!.Number!]); + ROMData.Put(0x1CD46, (byte)palGraphics[(int)mazeIsland.locationAtPalace4.Palace!.Number!]); + ROMData.Put(0x1CD42, (byte)palGraphics[(int)eastHyrule.locationAtPalace5.Palace!.Number!]); + ROMData.Put(0x1CD43, (byte)palGraphics[(int)eastHyrule.locationAtPalace6.Palace!.Number!]); + ROMData.Put(0x1CD44, (byte)palGraphics[(int)eastHyrule.locationAtGP.Palace!.Number!]); if (props.ShuffleEnemyPalettes) { @@ -2687,98 +2608,37 @@ private void UpdateRom() RerollPaletteTable(RomMap.TOWN_PALETTE_TABLE, customizationRng); RerollPaletteTable(RomMap.PALACE_PALETTE_TABLE_MAJOR, customizationRng); RerollPaletteTable(RomMap.GP_PALETTE_TABLE_MAJOR, customizationRng); - - /* - lets no longer shuffle orange/red/blue bits for enemies, it changes - what is visible in the dark and also doesn't add enough to be worth it imo. - I also prefer if we can have the palette shuffle as a customize (non-flag) option. - - for (int i = RomMap.WEST_ENEMY_STATS_TABLE + 0x3; i < RomMap.WEST_ENEMY_STATS_TABLE + 0x23; i++) - { - if (i != RomMap.WEST_ENEMY_STATS_TABLE + 0x13) // skip elevator - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x94e8; i < 0x9508; i++) - { - if (i != 0x94f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x114e8; i < 0x11508; i++) - { - if (i != 0x114f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x129e8; i < 0x12a09; i++) - { - if (i != 0x129f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - for (int i = 0x154e8; i < 0x15508; i++) - { - if (i != 0x154f8) - { - int b = ROMData.GetByte(i); - int p = b & 0x3F; - int n = r.Next(4); - n = n << 6; - ROMData.Put(i, (byte)(n + p)); - } - } - */ } //WRITE UPDATES TO WIZARD/QUEST COLLECTABLES HERE - ROMData.Put(RomMap.WEST_TROPHY_CAVE_COLLECTABLE, (byte)westHyrule.trophyCave.Collectables[0]); - ROMData.Put(RomMap.WEST_MAGIC_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.magicContainerCave.Collectables[0]); - ROMData.Put(RomMap.WEST_HEART_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.heartContainerCave.Collectables[0]); - ROMData.Put(RomMap.WEST_MEDICINE_CAVE_COLLECTABLE, (byte)westHyrule.medicineCave.Collectables[0]); - ROMData.Put(RomMap.WEST_GRASS_TILE_COLLECTABLE, (byte)westHyrule.grassTile.Collectables[0]); - ROMData.Put(RomMap.DM_SPECTACLE_ROCK_COLLECTABLE, (byte)deathMountain.specRock.Collectables[0]); - ROMData.Put(RomMap.DM_HAMMER_CAVE_COLLECTABLE, (byte)deathMountain.hammerCave.Collectables[0]); - ROMData.Put(RomMap.EAST_WATER_TILE_COLLECTABLE, (byte)eastHyrule.waterTile.Collectables[0]); - ROMData.Put(RomMap.EAST_DESERT_TILE_COLLECTABLE, (byte)eastHyrule.desertTile.Collectables[0]); + ROMData.Put(RomMap.WEST_TROPHY_CAVE_COLLECTABLE, (byte)westHyrule.trophyCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_MAGIC_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.magicContainerCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_HEART_CONTAINER_CAVE_COLLECTABLE, (byte)westHyrule.heartContainerCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_MEDICINE_CAVE_COLLECTABLE, (byte)westHyrule.medicineCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.WEST_GRASS_TILE_COLLECTABLE, (byte)westHyrule.grassTile.GetAllCollectables()[0]); + ROMData.Put(RomMap.DM_SPECTACLE_ROCK_COLLECTABLE, (byte)deathMountain.specRock.GetAllCollectables()[0]); + ROMData.Put(RomMap.DM_HAMMER_CAVE_COLLECTABLE, (byte)deathMountain.hammerCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_WATER_TILE_COLLECTABLE, (byte)eastHyrule.waterTile.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_DESERT_TILE_COLLECTABLE, (byte)eastHyrule.desertTile.GetAllCollectables()[0]); ROMData.ElevatorBossFix(props.BossItem); // Update item rooms and entrances for all palaces - Location[] locations = [ + Location[] palaceLocations = [ westHyrule.locationAtPalace1, westHyrule.locationAtPalace2, westHyrule.locationAtPalace3, eastHyrule.locationAtPalace5, eastHyrule.locationAtPalace6, eastHyrule.locationAtGP, mazeIsland.locationAtPalace4, ]; - foreach (var loc in locations) + foreach (var loc in palaceLocations) { - var idx = (int)loc.PalaceNumber! - 1; - if (loc.PalaceNumber != 7) + var idx = (int)loc.Palace!.Number! - 1; + if (loc.Palace!.Number != 7) { - for(int i = 0; i < palaces[idx].ItemRooms.Count; i++) + foreach(Room itemRoom in loc.Palace.ItemRooms) { - ROMData.UpdateItem(loc.Collectables[i], palaces[idx].ItemRooms[i]); + Debug.Assert(itemRoom.Collectable != null, $"Item room {itemRoom.Name} map {itemRoom.Map} in palace {loc.Palace.Number} is missing its item"); + ROMData.UpdateItem((Collectable)itemRoom.Collectable, itemRoom); } } @@ -2790,33 +2650,33 @@ what is visible in the dark and also doesn't add enough to be worth it imo. ROMData.Put(loc.MemAddress + 0x7e, root.Map); } - ROMData.Put(RomMap.EAST_SPELL_TOWER_COLLECTABLE, (byte)eastHyrule.spellTower.Collectables[0]); //map 47 - ROMData.Put(RomMap.EAST_NEW_KASUTO_BASEMENT_COLLECTABLE, (byte)eastHyrule.newKasutoBasement.Collectables[0]); //map 46 + ROMData.Put(RomMap.EAST_SPELL_TOWER_COLLECTABLE, (byte)eastHyrule.newKasuto.Town!.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!.Collectable!); + ROMData.Put(RomMap.EAST_NEW_KASUTO_BASEMENT_COLLECTABLE, (byte)eastHyrule.newKasuto.Town!.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.Collectable!); - ROMData.Put(RomMap.MI_MAGIC_CONTAINER_DROP_COLLECTABLE, (byte)mazeIsland.magicContainerDrop.Collectables[0]); - ROMData.Put(RomMap.MI_CHILD_DROP_COLLECTABLE, (byte)mazeIsland.childDrop.Collectables[0]); + ROMData.Put(RomMap.MI_MAGIC_CONTAINER_DROP_COLLECTABLE, (byte)mazeIsland.magicContainerDrop.GetAllCollectables()[0]); + ROMData.Put(RomMap.MI_CHILD_DROP_COLLECTABLE, (byte)mazeIsland.childDrop.GetAllCollectables()[0]); if (props.PbagItemShuffle) { - ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.Collectables[0]); - ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.Collectables[0]); - ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.Collectables[0]); + ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.GetAllCollectables()[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.GetAllCollectables()[0]); } foreach (Location location in pbagHearts) { if (location == westHyrule.pbagCave) { - ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.Collectables[0]); + ROMData.Put(RomMap.WEST_PBAG_CAVE_COLLECTABLE, (byte)westHyrule.pbagCave.GetAllCollectables()[0]); } if (location == eastHyrule.pbagCave1) { - ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.Collectables[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE1_COLLECTABLE, (byte)eastHyrule.pbagCave1.GetAllCollectables()[0]); } if (location == eastHyrule.pbagCave2) { - ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.Collectables[0]); + ROMData.Put(RomMap.EAST_PBAG_CAVE2_COLLECTABLE, (byte)eastHyrule.pbagCave2.GetAllCollectables()[0]); } } @@ -2861,12 +2721,12 @@ what is visible in the dark and also doesn't add enough to be worth it imo. } //Update world check for p7 - if (westHyrule.locationAtPalace1.PalaceNumber == 7 || westHyrule.locationAtPalace2.PalaceNumber == 7 || westHyrule.locationAtPalace3.PalaceNumber == 7) + if (westHyrule.locationAtPalace1.Palace!.Number == 7 || westHyrule.locationAtPalace2.Palace!.Number == 7 || westHyrule.locationAtPalace3.Palace!.Number == 7) { ROMData.Put(0x1dd3b, 0x05); } - if (mazeIsland.locationAtPalace4.PalaceNumber == 7) + if (mazeIsland.locationAtPalace4.Palace!.Number == 7) { ROMData.Put(0x1dd3b, 0x14); } @@ -2903,8 +2763,8 @@ what is visible in the dark and also doesn't add enough to be worth it imo. { // Use the old spell menu behavior where the spells are placed in the menu in the location it came from var wizardCollectables = AllLocationsForReal() - .Where(l => l.ActualTown != null && Towns.STRICT_SPELL_LOCATIONS.Contains((Town)l.ActualTown)) - .Select(l => l.Collectables[0]).ToList(); + .Where(l => l.Town?.GetWizard() != null) + .Select(l => (Collectable)l.Town!.GetWizard()!.Collectable!).ToList(); for (int i = 0; i < magFunction.Length; i++) { @@ -3122,6 +2982,9 @@ public void RandomizeSmallItems(int world, bool first) public void PrintDebugSpoiler(LogLevel logLevel) { + //I didn't want to port this and we mostly use spoiler log for this now anyway so maybe this just dies. + throw new NotImplementedException(); + /* logger.Log(logLevel, "ITEMS:"); List magicContainerLocations = []; List heartContainerLocations = []; @@ -3170,6 +3033,7 @@ public void PrintDebugSpoiler(LogLevel logLevel) sb.AppendLine(locationName); } logger.Log(logLevel, sb.ToString()); + */ } public string GenerateSpoiler() @@ -3286,14 +3150,6 @@ public IEnumerable AllLocationsForReal() return locations ?? []; } - public IEnumerable GetNonSideviewItemLocations() - { - return AllLocationsForReal().Where( - i => i.VanillaCollectable.IsSpell() - || i.VanillaCollectable.IsQuestItem() - || i.VanillaCollectable.IsSwordTech()); - } - public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) { StringBuilder sb = new StringBuilder(); @@ -3308,7 +3164,7 @@ public void PrintRoutingDebug(int count, int wh, int eh, int dm, int mi) //Where(i => !i.ItemGet && !i.Collectable.IsInternalUse() foreach (Location location in AllLocationsForReal()) { - foreach (Collectable collectable in location.Collectables) + foreach (Collectable collectable in location.GetAllCollectables()) { if (!collectable.IsMinorItem() && !ItemGet[collectable]) { @@ -3344,17 +3200,20 @@ private bool IsRaftAlwaysRequired(RandomizerProperties props) return false; } - private void FullItemShuffle(Assembler asm, IEnumerable nonSideviewLocations) + private void FullItemShuffle(Assembler asm) { var a = asm.Module(); foreach (var collect in Enum.GetValues()) { a.Set($"{collect.ToString().ToUpper()}_ITEMLOC", (int)collect); } + throw new NotImplementedException("Is this needed anymore? Debug this for a refactor."); + /* foreach (var loc in nonSideviewLocations) { - a.Set($"{loc.VanillaCollectable.ToString().ToUpper()}_ITEMLOC", (int)loc.Collectables[0]); + //a.Set($"{loc.VanillaCollectable.ToString().ToUpper()}_ITEMLOC", (int)loc.Collectables[0]); } + */ foreach (var val in Enum.GetValues()) { a.Set($"{val.ToString()}_DIALOG_WEST_INDEX", (int)val); @@ -3539,13 +3398,13 @@ beq @EndOfData public void SetPalacePalettes(Assembler asm) { var a = asm.Module(); - a.Assign("P1Palette", (byte)palPalettes[westHyrule?.locationAtPalace1.PalaceNumber ?? 0]); - a.Assign("P2Palette", (byte)palPalettes[westHyrule?.locationAtPalace2.PalaceNumber ?? 1]); - a.Assign("P3Palette", (byte)palPalettes[westHyrule?.locationAtPalace3.PalaceNumber ?? 2]); - a.Assign("P4Palette", (byte)palPalettes[mazeIsland?.locationAtPalace4.PalaceNumber ?? 3]); - a.Assign("P5Palette", (byte)palPalettes[eastHyrule?.locationAtPalace5.PalaceNumber ?? 4]); - a.Assign("P6Palette", (byte)palPalettes[eastHyrule?.locationAtPalace6.PalaceNumber ?? 5]); - a.Assign("PGreatPalette", (byte)palPalettes[eastHyrule?.locationAtGP.PalaceNumber ?? 6]); + a.Assign("P1Palette", (byte)palPalettes[westHyrule?.locationAtPalace1.Palace!.Number ?? 0]); + a.Assign("P2Palette", (byte)palPalettes[westHyrule?.locationAtPalace2.Palace!.Number ?? 1]); + a.Assign("P3Palette", (byte)palPalettes[westHyrule?.locationAtPalace3.Palace!.Number ?? 2]); + a.Assign("P4Palette", (byte)palPalettes[mazeIsland?.locationAtPalace4.Palace!.Number ?? 3]); + a.Assign("P5Palette", (byte)palPalettes[eastHyrule?.locationAtPalace5.Palace!.Number ?? 4]); + a.Assign("P6Palette", (byte)palPalettes[eastHyrule?.locationAtPalace6.Palace!.Number ?? 5]); + a.Assign("PGreatPalette", (byte)palPalettes[eastHyrule?.locationAtGP.Palace!.Number ?? 6]); a.Code(""" .include "z2r.inc" @@ -3733,13 +3592,13 @@ private static void UpdateTexts(Assembler asm, List hints) private void AssignRealPalaceLocations(AsmModule a) { - a.Assign("RealPalaceAtLocation1", (westHyrule?.locationAtPalace1.PalaceNumber ?? 1) - 1); - a.Assign("RealPalaceAtLocation2", (westHyrule?.locationAtPalace2.PalaceNumber ?? 2) - 1); - a.Assign("RealPalaceAtLocation3", (westHyrule?.locationAtPalace3.PalaceNumber ?? 3) - 1); - a.Assign("RealPalaceAtLocation4", (mazeIsland?.locationAtPalace4.PalaceNumber ?? 4) - 1); - a.Assign("RealPalaceAtLocation5", (eastHyrule?.locationAtPalace5.PalaceNumber ?? 5) - 1); - a.Assign("RealPalaceAtLocation6", (eastHyrule?.locationAtPalace6.PalaceNumber ?? 6) - 1); - a.Assign("RealPalaceAtLocationGP", (eastHyrule?.locationAtGP.PalaceNumber ?? 7) - 1); + a.Assign("RealPalaceAtLocation1", (westHyrule?.locationAtPalace1.Palace!.Number ?? 1) - 1); + a.Assign("RealPalaceAtLocation2", (westHyrule?.locationAtPalace2.Palace!.Number ?? 2) - 1); + a.Assign("RealPalaceAtLocation3", (westHyrule?.locationAtPalace3.Palace!.Number ?? 3) - 1); + a.Assign("RealPalaceAtLocation4", (mazeIsland?.locationAtPalace4.Palace!.Number ?? 4) - 1); + a.Assign("RealPalaceAtLocation5", (eastHyrule?.locationAtPalace5.Palace!.Number ?? 5) - 1); + a.Assign("RealPalaceAtLocation6", (eastHyrule?.locationAtPalace6.Palace!.Number ?? 6) - 1); + a.Assign("RealPalaceAtLocationGP", (eastHyrule?.locationAtGP.Palace!.Number ?? 7) - 1); } public void StatTracking(RandomizerProperties props, Assembler asm) @@ -3861,7 +3720,7 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando rom.AddRandomizerToTitle(engine, props.MarioMode, IS_RANDOMIZED); AddCropGuideBoxesToFileSelect(engine); FixHelmetheadBossRoom(engine); - FullItemShuffle(engine, GetNonSideviewItemLocations()); + FullItemShuffle(engine); rom.DontCountExpDuringTalking(engine); rom.FixElevatorPositionInFallRooms(engine); rom.AllowForChangingDoorYPosition(engine); @@ -4040,42 +3899,4 @@ void PatchAddress(byte opcode, int before, int after) UpdateTexts(engine, texts); } - - //This entire town location shuffle structure is awful if this method needs to exist. - private Location GetTownLocation(Town town) - { - if(westHyrule.locationAtMido.ActualTown == town) - { - return westHyrule.locationAtMido; - } - if (westHyrule.locationAtRauru.ActualTown == town) - { - return westHyrule.locationAtRauru; - } - if (westHyrule.locationAtRuto.ActualTown == town) - { - return westHyrule.locationAtRuto; - } - if (westHyrule.locationAtSariaNorth.ActualTown == town) - { - return westHyrule.locationAtSariaNorth; - } - if (eastHyrule.townAtDarunia.ActualTown == town) - { - return eastHyrule.townAtDarunia; - } - if (eastHyrule.townAtNabooru.ActualTown == town) - { - return eastHyrule.townAtNabooru; - } - if (eastHyrule.townAtNewKasuto.ActualTown == town) - { - return eastHyrule.townAtNewKasuto; - } - if (eastHyrule.townAtOldKasuto.ActualTown == town) - { - return eastHyrule.townAtOldKasuto; - } - throw new Exception("Unrecognized town in GetTownLocation, was this extended to non-standard wizards?"); - } } diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index 42a4b9a08..899d6acc5 100644 --- a/RandomizerCore/Music.cs +++ b/RandomizerCore/Music.cs @@ -11,6 +11,7 @@ using System.Net.Http.Headers; using System.Text; using Z2Randomizer.RandomizerCore.Overworld; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -372,7 +373,7 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) usage = Usage.Town; else if (loc.TerrainType == Terrain.TOWN) { - if (loc.ActualTown == Town.OLD_KASUTO // ?? + if (loc.Town?.Type == TownType.OLD_KASUTO // ?? /*|| loc.ActualTown == Town.SARIA_SOUTH*/) continue; @@ -380,10 +381,10 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) } else if (loc.TerrainType == Terrain.PALACE) { - if (loc.PalaceNumber == null) + if (loc.Palace?.Number == null) continue; // North palace - usage = (loc.PalaceNumber < 7) + usage = (loc.Palace.Number < 7) ? Usage.Palace : Usage.GreatPalace; } @@ -407,7 +408,7 @@ SongMap CreateAreaSongMap(UsesSongs usesSongs) Func GetSongIdx = usage switch { // One song per palace - Usage.Palace => (loc => (int)loc.PalaceNumber! - 1), + Usage.Palace => (loc => (int)loc.Palace!.Number! - 1), Usage.GreatPalace => (loc => 0), // One song per continent _ => (loc => (int)(loc.VanillaContinent ?? loc.Continent)), diff --git a/RandomizerCore/Overworld/DeathMountain.cs b/RandomizerCore/Overworld/DeathMountain.cs index c2bcbdeb2..d00f67537 100644 --- a/RandomizerCore/Overworld/DeathMountain.cs +++ b/RandomizerCore/Overworld/DeathMountain.cs @@ -193,7 +193,7 @@ .. rom.LoadLocations(LocationID.DM_SPEC_ROCK, 1, terrains), climate = Climates.Create(props.DmClimate); climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit()); - SetVanillaCollectables(props.ReplaceFireWithDash); + //SetVanillaCollectables(props.ReplaceFireWithDash); } private void RemoveLocationsDM(ICollection locations) @@ -1114,7 +1114,7 @@ public override void UpdateVisit(List requireables) if (location.AccessRequirements.AreSatisfiedBy(requireables)) { location.Reachable = true; - if (connectionsDM.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables)) + if (connectionsDM.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) { foreach (Location connectedLocation in connectionsDM[location]) { @@ -1215,18 +1215,20 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useDash) { hammerCave.VanillaCollectable = Collectable.HAMMER; specRock.VanillaCollectable = Collectable.MAGIC_CONTAINER; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("DEATH MOUNTAIN: "); - sb.AppendLine("\tHammer Cave: " + hammerCave.Collectables[0].EnglishText()); - sb.AppendLine("\tSpec Rock: " + specRock.Collectables[0].EnglishText()); + sb.AppendLine("\tHammer Cave: " + hammerCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tSpec Rock: " + specRock.GetAllCollectables()[0].EnglishText()); sb.AppendLine(); return sb.ToString(); @@ -1234,17 +1236,25 @@ public override string GenerateSpoiler() public override void DisableDisallowedPassthroughs() { - foreach (Location location in Locations[Terrain.CAVE]) - { - location.IsPassthrough = false; - } - foreach (Location location in Locations[Terrain.TOWN]) - { - location.IsPassthrough = false; - } - foreach (Location location in Locations[Terrain.PALACE]) - { - location.IsPassthrough = false; - } + foreach (Location location in Locations[Terrain.CAVE]) + { + location.IsPassthrough = false; + } + foreach (Location location in Locations[Terrain.TOWN]) + { + location.IsPassthrough = false; + } + foreach (Location location in Locations[Terrain.PALACE]) + { + location.IsPassthrough = false; + } + } + + public override void ResetCollectables(RandomizerProperties props) + { + hammerCave.SetCollectables([Collectable.HAMMER]); + hammerCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + specRock.SetCollectables([Collectable.MAGIC_CONTAINER]); + specRock.CollectablesAreShufflable = props.ShuffleOverworldItems; } } diff --git a/RandomizerCore/Overworld/EastHyrule.cs b/RandomizerCore/Overworld/EastHyrule.cs index 5a8b46a66..4b045c3d2 100644 --- a/RandomizerCore/Overworld/EastHyrule.cs +++ b/RandomizerCore/Overworld/EastHyrule.cs @@ -5,6 +5,7 @@ using System.Text; using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -12,7 +13,6 @@ namespace Z2Randomizer.RandomizerCore.Overworld; //6A35 - address in memory of palace 6 y coord public sealed class EastHyrule : World { - //private int bridgeCount; int debug = 0; private static readonly new Logger logger = LogManager.GetCurrentClassLogger(); @@ -65,16 +65,12 @@ public sealed class EastHyrule : World public Location locationAtPalace5; public Location locationAtPalace6; - public Location fountain; public Location waterTile; public Location desertTile; - public Location townAtDarunia; - public Location daruniaRoof; - public Location townAtNewKasuto; - public Location spellTower; - public Location newKasutoBasement; - public Location townAtNabooru; - public Location townAtOldKasuto; + public Location darunia; + public Location newKasuto; + public Location nabooru; + public Location oldKasuto; public Location locationAtGP; public Location pbagCave1; public Location pbagCave2; @@ -140,43 +136,28 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains), //Palaces locationAtPalace5 = GetLocation(LocationID.EAST_PALACE5); - locationAtPalace5.PalaceNumber = 5; - locationAtPalace5.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; - locationAtPalace6 = GetLocation(LocationID.EAST_PALACE6); - locationAtPalace6.PalaceNumber = 6; - locationAtPalace6.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; - locationAtGP = GetLocation(LocationID.EAST_GREAT_PALACE); - locationAtGP.PalaceNumber = 7; - locationAtGP.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; // the same as this gets shuffled with regular palaces - locationAtGP.VanillaCollectable = Collectable.DO_NOT_USE; - locationAtGP.Collectables = []; //Towns - townAtNabooru = GetLocation(LocationID.EAST_TOWN_NABOORU); - townAtNabooru.VanillaCollectable = props.ReplaceFireWithDash ? Collectable.DASH_SPELL : Collectable.FIRE_SPELL; - townAtNabooru.Collectables = props.ReplaceFireWithDash ? [Collectable.DASH_SPELL] : [Collectable.FIRE_SPELL]; - townAtNabooru.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.WATER]) - : new Requirements([], [[RequirementType.WATER, RequirementType.FIVE_CONTAINERS]]); - - townAtDarunia = GetLocation(LocationID.EAST_TOWN_DARUNIA); - townAtDarunia.VanillaCollectable = Collectable.REFLECT_SPELL; - townAtDarunia.Collectables = [Collectable.REFLECT_SPELL]; - townAtDarunia.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.CHILD]) - : new Requirements([], [[RequirementType.CHILD, RequirementType.SIX_CONTAINERS]]); - - townAtNewKasuto = GetLocation(LocationID.EAST_TOWN_NEW_KASUTO); - townAtNewKasuto.VanillaCollectable = Collectable.SPELL_SPELL; - townAtNewKasuto.Collectables = [Collectable.SPELL_SPELL]; - townAtNewKasuto.CollectableRequirements = props.DisableMagicRecs ? Requirements.NONE : new Requirements([RequirementType.SEVEN_CONTAINERS]); - - townAtOldKasuto = GetLocation(LocationID.EAST_TOWN_OLD_KASUTO); - townAtOldKasuto.VanillaCollectable = Collectable.THUNDER_SPELL; - townAtOldKasuto.Collectables = [Collectable.THUNDER_SPELL]; - townAtOldKasuto.CollectableRequirements = props.DisableMagicRecs ? Requirements.NONE : new Requirements([RequirementType.EIGHT_CONTAINERS]); + nabooru = GetLocation(LocationID.EAST_TOWN_NABOORU); + nabooru.Town = Towns.LoadVanillaTown(rom, TownType.NABOORU, props.DisableMagicRecs); + + darunia = GetLocation(LocationID.EAST_TOWN_DARUNIA); + darunia.Town = Towns.LoadVanillaTown(rom, TownType.DARUNIA, props.DisableMagicRecs); + + newKasuto = GetLocation(LocationID.EAST_TOWN_NEW_KASUTO); + newKasuto.Town = Towns.LoadVanillaTown(rom, TownType.NEW_KASUTO, props.DisableMagicRecs); + newKasuto.Town.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.AccessRequirements = props.NewKasutoBasementRequirement switch + { + 5 => new Requirements([RequirementType.FIVE_CONTAINERS]), + 6 => new Requirements([RequirementType.SIX_CONTAINERS]), + 7 => new Requirements([RequirementType.SEVEN_CONTAINERS]), + _ => throw new Exception($"Unsupported New Kasuto basement container count: {props.NewKasutoBasementRequirement}") + }; + + oldKasuto = GetLocation(LocationID.EAST_TOWN_OLD_KASUTO); + oldKasuto.Town = Towns.LoadVanillaTown(rom, TownType.OLD_KASUTO, props.DisableMagicRecs); waterTile = GetLocation(LocationID.EAST_WATER); waterTile.AccessRequirements = new Requirements([RequirementType.BOOTS]); @@ -228,52 +209,6 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains), pbagCave2 = GetLocation(LocationID.EAST_CAVE_PBAG2); VANILLA_MAP_ADDR = 0x9056; - //Fake locations that dont correspond to anywhere on the map, but still hold logic and items - spellTower = new Location(townAtNewKasuto); - spellTower.VanillaCollectable = Collectable.MAGIC_KEY; - spellTower.Collectables = [Collectable.MAGIC_KEY]; - spellTower.CollectableRequirements = new Requirements([RequirementType.SPELL]); - spellTower.Name = "Spell Tower"; - spellTower.CanShuffle = false; - spellTower.ActualTown = null; - townAtNewKasuto.Children.Add(spellTower); - AddLocation(spellTower); - - newKasutoBasement = new Location(townAtNewKasuto); - newKasutoBasement.VanillaCollectable = Collectable.MAGIC_CONTAINER; - newKasutoBasement.Collectables = [Collectable.MAGIC_CONTAINER]; - newKasutoBasement.CollectableRequirements = props.NewKasutoBasementRequirement switch - { - 5 => new Requirements([RequirementType.FIVE_CONTAINERS]), - 6 => new Requirements([RequirementType.SIX_CONTAINERS]), - 7 => new Requirements([RequirementType.SEVEN_CONTAINERS]), - _ => throw new Exception($"Unsupported New Kasuto basement container count: {props.NewKasutoBasementRequirement}") - }; - newKasutoBasement.Name = "Granny's basement"; - newKasutoBasement.CanShuffle = false; - newKasutoBasement.ActualTown = null; - townAtNewKasuto.Children.Add(newKasutoBasement); - AddLocation(newKasutoBasement); - - fountain = new Location(townAtNabooru); - fountain.VanillaCollectable = Collectable.WATER; - fountain.Collectables = [Collectable.WATER]; - fountain.Name = "Water Fountain"; - fountain.ActualTown = Town.NABOORU_FOUNTAIN; - fountain.CanShuffle = false; - townAtNabooru.Children.Add(fountain); - AddLocation(fountain); - - daruniaRoof = new Location(townAtDarunia); - daruniaRoof.VanillaCollectable = Collectable.UPSTAB; - daruniaRoof.Collectables = [Collectable.UPSTAB]; - daruniaRoof.CollectableRequirements = new Requirements([RequirementType.FAIRY, RequirementType.JUMP]); - daruniaRoof.Name = "Darunia Roof"; - daruniaRoof.ActualTown = Town.DARUNIA_ROOF; - daruniaRoof.CanShuffle = false; - townAtDarunia.Children.Add(daruniaRoof); - AddLocation(daruniaRoof); - walkableTerrains = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE]; randomTerrainFilter = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE, Terrain.MOUNTAIN, Terrain.WALKABLEWATER]; @@ -357,17 +292,15 @@ .. rom.LoadLocations(LocationID.EAST_TOWN_OLD_KASUTO, 4, terrains), { (0x66, 0x2D), "south" }, { (0x49, 0x04), "gp" } }; - townAtNewKasuto.IsExternalWorld = true; + newKasuto.IsExternalWorld = true; locationAtPalace6.IsExternalWorld = true; hiddenPalaceLocation = locationAtPalace6; - hiddenKasutoLocation = townAtNewKasuto; + hiddenKasutoLocation = newKasuto; //Climate filtering climate = Climates.Create(props.EastClimate); climate.SeedTerrainCount = Math.Min(climate.SeedTerrainCount, biome.SeedTerrainLimit()); climate.DisallowTerrain(props.CanWalkOnWaterWithBoots ? Terrain.WATER : Terrain.WALKABLEWATER); - //climate.DisallowTerrain(Terrain.LAVA); - SetVanillaCollectables(props.ReplaceFireWithDash); } public override bool Terraform(RandomizerProperties props, ROM rom) @@ -435,7 +368,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) if (!props.ShuffleHidden) { - townAtNewKasuto.CanShuffle = false; + newKasuto.CanShuffle = false; locationAtPalace6.CanShuffle = false; } ShuffleLocations(AllLocations); @@ -806,7 +739,7 @@ public override bool Terraform(RandomizerProperties props, ROM rom) if(props.RiverDevilBlockerOption == RiverDevilBlockerOption.SIEGE) { //Iterate the towns in a random order - Location[] towns = [townAtNabooru, townAtDarunia, townAtNewKasuto, townAtOldKasuto]; + Location[] towns = [nabooru, darunia, newKasuto, oldKasuto]; RNG.Shuffle(towns); bool placed = false; foreach (Location location in towns) @@ -863,17 +796,13 @@ public override bool Terraform(RandomizerProperties props, ROM rom) if (props.HiddenPalace) { - rom.UpdateHiddenPalaceSpot(biome, hiddenPalaceCoords, hiddenPalaceLocation, - townAtNewKasuto, spellTower, !props.LegacyVanillaShuffledLocations); - hiddenPalaceLocation.AccessRequirements = hiddenPalaceLocation.AccessRequirements.WithHardRequirement(RequirementType.FLUTE); - hiddenPalaceLocation.Children.ForEach(i => i.AccessRequirements = i.AccessRequirements.WithHardRequirement(RequirementType.FLUTE)); - } + rom.UpdateHiddenPalaceSpot(biome, hiddenPalaceCoords, hiddenPalaceLocation, !props.LegacyVanillaShuffledLocations); + hiddenPalaceLocation.AccessRequirements = hiddenPalaceLocation.AccessRequirements.WithHardRequirement(RequirementType.FLUTE); } if (props.HiddenKasuto) { - rom.UpdateKasuto(hiddenKasutoLocation, townAtNewKasuto, spellTower, biome, + rom.UpdateKasuto(hiddenKasutoLocation, biome, baseAddr, terrains[hiddenKasutoLocation.ID], !props.LegacyVanillaShuffledLocations); hiddenKasutoLocation.AccessRequirements = hiddenPalaceLocation.AccessRequirements.WithHardRequirement(RequirementType.HAMMER); - hiddenKasutoLocation.Children.ForEach(i => i.AccessRequirements = i.AccessRequirements.WithHardRequirement(RequirementType.HAMMER)); } WriteMapToRom(rom, true, MAP_ADDR, MAP_SIZE_BYTES, hiddenPalaceLocation.Y, hiddenPalaceLocation.Xpos, props.HiddenPalace, props.HiddenKasuto); @@ -1558,7 +1487,7 @@ private void RandomizeHiddenKasuto(bool shuffleHidden) } else { - hiddenKasutoLocation = townAtNewKasuto; + hiddenKasutoLocation = newKasuto; } hiddenKasutoLocation.TerrainType = Terrain.FOREST; hiddenKasutoLocation.AccessRequirements = hiddenKasutoLocation.AccessRequirements.WithHardRequirement(RequirementType.HAMMER); @@ -1662,10 +1591,6 @@ private bool RandomizeHiddenPalace(ROM rom, bool shuffleHidden, bool hiddenKasut if (!AllReached) { base.UpdateAllReached(); - if (!hiddenPalaceLocation.Reachable || !hiddenKasutoLocation.Reachable || !spellTower.Reachable) - { - AllReached = false; - } } } @@ -1680,7 +1605,7 @@ public override void UpdateVisit(List requireables) if(location.AccessRequirements.AreSatisfiedBy(requireables)) { location.Reachable = true; - if (connections.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables)) + if (connections.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) { Location connectedLocation = connections[location]; connectedLocation.Reachable = true; @@ -1947,8 +1872,6 @@ protected override List GetPathingStarts() public bool ValidateBasicRouting() { List unreachedLocations = RequiredLocations(false, false).ToList(); - //SpellTower's connection logic isn't implemented here, nor do we care since we're assuming you have everything. - unreachedLocations.Remove(spellTower); bool[,] visitedCoordinates = new bool[MapRows, MapColumns]; List<(int, int)> pendingCoordinates = new(); @@ -2000,15 +1923,11 @@ public bool ValidateBasicRouting() } } while (pendingCoordinates.Count > 0); - return !unreachedLocations.Any(); + return unreachedLocations.Count != 0; } protected override void OnUpdateReachableTrigger() { - foreach(Location parentLocation in AllLocations.Where(i => i.Children != null && i.ActualTown != null)) - { - parentLocation.Children.ForEach(child => child.Reachable = parentLocation.Reachable); - } } public override string GetName() @@ -2024,11 +1943,10 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool locationAtPalace6, waterTile, desertTile, - townAtDarunia, - townAtNewKasuto, - spellTower, - townAtNabooru, - townAtOldKasuto, + darunia, + newKasuto, + nabooru, + oldKasuto, locationAtGP, pbagCave1, pbagCave2, @@ -2059,52 +1977,50 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useDash) { locationAtPalace5.VanillaCollectable = Collectable.FLUTE; locationAtPalace6.VanillaCollectable = Collectable.CROSS; - townAtNabooru.VanillaCollectable = useDash ? Collectable.DASH_SPELL : Collectable.FIRE_SPELL; + nabooru.VanillaCollectable = useDash ? Collectable.DASH_SPELL : Collectable.FIRE_SPELL; fountain.VanillaCollectable = Collectable.WATER; - townAtDarunia.VanillaCollectable = Collectable.REFLECT_SPELL; + darunia.VanillaCollectable = Collectable.REFLECT_SPELL; daruniaRoof.VanillaCollectable = Collectable.UPSTAB; newKasutoBasement.VanillaCollectable = Collectable.MAGIC_CONTAINER; - townAtNewKasuto.VanillaCollectable = Collectable.SPELL_SPELL; + newKasuto.VanillaCollectable = Collectable.SPELL_SPELL; spellTower.VanillaCollectable = Collectable.MAGIC_KEY; - townAtOldKasuto.VanillaCollectable = Collectable.THUNDER_SPELL; + oldKasuto.VanillaCollectable = Collectable.THUNDER_SPELL; waterTile.VanillaCollectable = Collectable.HEART_CONTAINER; desertTile.VanillaCollectable = Collectable.HEART_CONTAINER; pbagCave1.VanillaCollectable = Collectable.XL_BAG; pbagCave2.VanillaCollectable = Collectable.XL_BAG; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("EAST: "); - sb.AppendLine("\tNabooru: " + AllLocations.First(i => i.ActualTown == Town.NABOORU).Collectables[0].EnglishText()); - sb.AppendLine("\tFountain: " + fountain.Collectables[0].EnglishText()); - sb.AppendLine("\tDarunia: " + AllLocations.First(i => i.ActualTown == Town.DARUNIA_WEST).Collectables[0].EnglishText()); - sb.AppendLine("\tUpstab Guy: " + daruniaRoof.Collectables[0].EnglishText()); - sb.AppendLine("\tNew Kasuto: " + AllLocations.First(i => i.ActualTown == Town.NEW_KASUTO).Collectables[0].EnglishText()); - sb.AppendLine("\tGranny's Basement: " + newKasutoBasement.Collectables[0].EnglishText()); - sb.AppendLine("\tSpell Tower: " + spellTower.Collectables[0].EnglishText()); - sb.AppendLine("\tOld Kasuto: " + AllLocations.First(i => i.ActualTown == Town.OLD_KASUTO).Collectables[0].EnglishText()); + sb.AppendLine("Nabooru: \n" + nabooru.Town!.GenerateSpoiler()); + sb.AppendLine("Darunia: \n" + nabooru.Town!.GenerateSpoiler()); + sb.AppendLine("New Kasuto: \n" + nabooru.Town!.GenerateSpoiler()); + sb.AppendLine("Old Kasuto: \n" + nabooru.Town!.GenerateSpoiler()); - sb.AppendLine("\tRisen Pbag Cave: " + pbagCave2.Collectables[0].EnglishText()); - sb.AppendLine("\tSunken Pbag Cave: " + pbagCave1.Collectables[0].EnglishText()); - sb.AppendLine("\tWater Tile: " + waterTile.Collectables[0].EnglishText()); - sb.AppendLine("\tDesert tile: " + desertTile.Collectables[0].EnglishText()); + sb.AppendLine("\tRisen Pbag Cave: " + pbagCave2.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tSunken Pbag Cave: " + pbagCave1.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tWater Tile: " + waterTile.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tDesert tile: " + desertTile.GetAllCollectables()[0].EnglishText()); - sb.Append("\tPalace 5 (" + locationAtPalace5.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace5.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace5.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 5 (" + locationAtPalace5.Palace!.Number + "): "); + sb.AppendLine(locationAtPalace5.GetAllCollectables().Count == 0 ? "No Items" : string.Join(", ", locationAtPalace5.GetAllCollectables().Select(c => c.EnglishText()))); - sb.Append("\tPalace 6 (" + locationAtPalace6.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace6.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace6.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 6 (" + locationAtPalace6.Palace!.Number + "): "); + sb.AppendLine(locationAtPalace6.GetAllCollectables().Count == 0 ? "No Items" : string.Join(", ", locationAtPalace6.GetAllCollectables().Select(c => c.EnglishText()))); - sb.Append("\tPalace 7 (" + locationAtGP.PalaceNumber + "): "); - sb.AppendLine(locationAtGP.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtGP.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 7 (" + locationAtGP.Palace!.Number + "): "); + sb.AppendLine(locationAtGP.GetAllCollectables().Count == 0 ? "No Items" : string.Join(", ", locationAtGP.GetAllCollectables().Select(c => c.EnglishText()))); sb.AppendLine(); return sb.ToString(); @@ -2161,4 +2077,36 @@ public override void DisableDisallowedPassthroughs() swamp.AccessRequirements = swamp.AccessRequirements.Without(RequirementType.FAIRY); } } + + public override void ResetCollectables(RandomizerProperties props) + { + nabooru.Town!.GetWizard()!.Collectable = props.ReplaceFireWithDash ? Collectable.DASH_SPELL : Collectable.FAIRY_SPELL; + nabooru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + nabooru.Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.Collectable = Collectable.WATER; + nabooru.Town!.GetTownMap(VanillaTownMap.NABOORU_MID)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + + darunia.Town!.GetWizard()!.Collectable = Collectable.REFLECT_SPELL; + darunia.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.Collectable = Collectable.UPSTAB; + darunia.Town!.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + + newKasuto.Town!.GetWizard()!.Collectable = Collectable.SPELL_SPELL; + newKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + newKasuto.Town!.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!.Collectable = Collectable.MAGIC_KEY; + newKasuto.Town!.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!.CollectableIsShufflable = props.ShuffleOverworldItems; + newKasuto.Town!.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.Collectable = Collectable.MAGIC_CONTAINER; + newKasuto.Town!.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!.CollectableIsShufflable = props.ShuffleOverworldItems; + + oldKasuto.Town!.GetWizard()!.Collectable = Collectable.THUNDER_SPELL; + oldKasuto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + desertTile.SetCollectables([Collectable.HEART_CONTAINER]); + desertTile.CollectablesAreShufflable = props.ShuffleOverworldItems; + waterTile.SetCollectables([Collectable.HEART_CONTAINER]); + waterTile.CollectablesAreShufflable = props.ShuffleOverworldItems; + pbagCave1.SetCollectables([Collectable.XL_BAG]); + pbagCave1.CollectablesAreShufflable = props.ShuffleOverworldItems && props.PbagItemShuffle; + pbagCave2.SetCollectables([Collectable.XL_BAG]); + pbagCave2.CollectablesAreShufflable = props.ShuffleOverworldItems && props.PbagItemShuffle; + } } \ No newline at end of file diff --git a/RandomizerCore/Overworld/Location.cs b/RandomizerCore/Overworld/Location.cs index 23536853e..142bd9b74 100644 --- a/RandomizerCore/Overworld/Location.cs +++ b/RandomizerCore/Overworld/Location.cs @@ -3,6 +3,9 @@ using System.Diagnostics; using System.Collections.Generic; using System.Linq; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; +using DynamicData; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -13,13 +16,14 @@ public class Location public LocationID ID { get; set; } - public List Collectables { get; set; } - public Collectable VanillaCollectable { get; set; } + private List NonPalaceTownCollectables { get; set; } + public bool CollectablesAreShufflable { get; set; } public bool AppearsOnMap { get; set; } public bool Reachable { get; set; } - public int? PalaceNumber { get; set; } - public Town? ActualTown { get; set; } + public Town? Town { get; set; } + public Palace? Palace { get; set; } + //public TownEnum? ActualTown { get; set; } public Continent Continent { get; set; } public Continent? VanillaContinent { get; set; } public Continent? ConnectedContinent { get; set; } @@ -27,7 +31,6 @@ public class Location public Terrain TerrainType { get; set; } //public byte[] LocationBytes { get; set; } - public List Children { get; set; } = []; public int MemAddress { get; set; } @@ -81,11 +84,6 @@ public int Xpos //List of requirements to access this location. Required before the other requirements are evaluated, //and to pass through the location on the map, even if the collectable is not gettable. public Requirements AccessRequirements; - //Requirements to get the thing(s) here. For palaces, this does not (yet) consider the layout of the palace or the rooms in it - //only the general "need fairy or key" requirements for palaces generally - public Requirements CollectableRequirements; - //If this location is a connector, these are the requirements for the other side of the connection to be reached - public Requirements ConnectionRequirements; //This does 2 things and should only do 1. It both tracks whether the location is a location that should be possible to shuffle, //and tracks whether this location has actually been shuffled already. This persistence doesn't always get reset properly, @@ -151,20 +149,21 @@ public Location(LocationID lid, int yPos, int xPos, int map, Continent continent MemAddress = lid.GetRomOffset(); EntranceNumber = 0; CanShuffle = true; - Collectables = []; + NonPalaceTownCollectables = []; Reachable = false; - PalaceNumber = null; + Palace = null; Continent = continent; VanillaContinent = lid.GetContinent(); ConnectedContinent = null; AccessRequirements = Requirements.NONE; - CollectableRequirements = Requirements.NONE; - ConnectionRequirements = Requirements.NONE; IsPassthrough = isPassthrough; WasPassthrough = isPassthrough; Name = ID.GetName() ?? "Unknown (" + Continent.GetName(Continent) + ")"; - ActualTown = ID.GetTown(); + if (Town != null) + { + Town.Type = ID.GetTown(); + } if (Name.StartsWith("Unknown") && Xpos != 0 && YRaw != 0) { @@ -212,7 +211,7 @@ public string GetDebuggerDisplay() + " " + Name + " (" + (Y) + "," + (Xpos) + ") _" + (Reachable ? "Reachable " : "Unreachable ") - + '[' + string.Join(", ", Collectables.Select(i => i.ToString())) + ']'; + + '[' + string.Join(", ", GetAllCollectables().Select(i => i.ToString())) + ']'; } public void ResetCoords() @@ -223,7 +222,7 @@ public void ResetCoords() public int GetWorld() { //Towns reference their banks - if (ActualTown != null) + if (Town?.Type != null) { return Continent == Continent.WEST ? 4 : 10; } @@ -246,12 +245,10 @@ public int GetWorld() } //Palaces... have world numbers that kind... of... make sense? //This is what they are. - if (PalaceNumber != null) + if (Palace != null) { - return PalaceNumber switch + return Palace.Number switch { - //North palace - null => 0, 1 => 0b00001100 + (int)Continent, //12, 2 => 0b00001100 + (int)Continent, //12, 3 => 0b00010000 + (int)Continent, //16, @@ -277,4 +274,75 @@ public void Clear() EntranceNumber = 0; CanShuffle = false; } + + public List GetAllCollectables() + { + return GetGettableItems(RequirementTypeExtensions.ALL); + } + + public List GetShufflableCollectables() + { + return GetGettableItems(RequirementTypeExtensions.ALL, ForceEnterRight ? Direction.EAST : Direction.WEST, true); + } + + public List GetGettableItems(List requireables) + { + return GetGettableItems(requireables, ForceEnterRight ? Direction.EAST : Direction.WEST); + } + + public List GetGettableItems(List requireables, Direction entranceDirection, bool shufflableItemsOnly = false) + { + if (Town != null) + { + return Town.GetGettableItems(entranceDirection, requireables, shufflableItemsOnly); + } + if (Palace != null) + { + return Palace.GetGettableItems(requireables, shufflableItemsOnly); + } + if(shufflableItemsOnly && !CollectablesAreShufflable) + { + return []; + } + return AccessRequirements.AreSatisfiedBy(requireables) ? NonPalaceTownCollectables : []; + } + + public void SetCollectables(IEnumerable collectables) + { + Debug.Assert(Town == null || Palace == null); + if (Town != null) + { + Town.SetCollectables(collectables); + } + else if (Palace != null) + { + Palace.SetCollectables(collectables); + } + else NonPalaceTownCollectables = [.. collectables]; + } + + /// + /// Replaces the first instance of a given collectable at a location with the indicated collectable. + /// Intended to be used to replace unique items, and "first" is currently undefined in order so be careful (or fix this) + /// + /// + /// + public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) + { + Debug.Assert(Town == null || Palace == null); + if (Town != null) + { + return Town.ReplaceCollectable(toReplace, replacement); + } + else if (Palace != null) + { + return Palace.ReplaceCollectable(toReplace, replacement); + } + else if (NonPalaceTownCollectables.Contains(toReplace)) + { + NonPalaceTownCollectables.Replace(toReplace, replacement); + return true; + } + return false; + } } diff --git a/RandomizerCore/Overworld/LocationID.cs b/RandomizerCore/Overworld/LocationID.cs index 2216f094a..7636772ea 100644 --- a/RandomizerCore/Overworld/LocationID.cs +++ b/RandomizerCore/Overworld/LocationID.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -276,18 +277,18 @@ public static IEnumerable EnumerateNamed(LocationID startLid, int li public static class LocationIDExtensions { - static readonly Dictionary townMap = new() + static readonly Dictionary townMap = new() { - [LocationID.WEST_TOWN_RAURO] = Town.RAURU, - [LocationID.WEST_TOWN_RUTO] = Town.RUTO, - [LocationID.WEST_TOWN_SARIA_NORTH] = Town.SARIA_NORTH, - [LocationID.WEST_TOWN_SARIA_SOUTH] = Town.SARIA_SOUTH, - [LocationID.WEST_BAGU_HOUSE] = Town.BAGU, - [LocationID.WEST_TOWN_MIDO] = Town.MIDO_WEST, - [LocationID.EAST_TOWN_NABOORU] = Town.NABOORU, - [LocationID.EAST_TOWN_DARUNIA] = Town.DARUNIA_WEST, - [LocationID.EAST_TOWN_OLD_KASUTO] = Town.OLD_KASUTO, - [LocationID.EAST_TOWN_NEW_KASUTO] = Town.NEW_KASUTO, + [LocationID.WEST_TOWN_RAURO] = TownType.RAURU, + [LocationID.WEST_TOWN_RUTO] = TownType.RUTO, + [LocationID.WEST_TOWN_SARIA_NORTH] = TownType.SARIA, + [LocationID.WEST_TOWN_SARIA_SOUTH] = TownType.SARIA, + [LocationID.WEST_BAGU_HOUSE] = TownType.BAGU, + [LocationID.WEST_TOWN_MIDO] = TownType.MIDO, + [LocationID.EAST_TOWN_NABOORU] = TownType.NABOORU, + [LocationID.EAST_TOWN_DARUNIA] = TownType.DARUNIA, + [LocationID.EAST_TOWN_OLD_KASUTO] = TownType.OLD_KASUTO, + [LocationID.EAST_TOWN_NEW_KASUTO] = TownType.NEW_KASUTO, }; public static bool IsNamed(this LocationID lid) @@ -338,9 +339,9 @@ public static int GetRomOffset(this LocationID lid, int byteIdx = 0) /// /// /// - public static Town? GetTown(this LocationID lid) + public static TownType? GetTown(this LocationID lid) { - if (townMap.TryGetValue(lid, out Town town)) + if (townMap.TryGetValue(lid, out TownType town)) return town; return null; diff --git a/RandomizerCore/Overworld/MazeIsland.cs b/RandomizerCore/Overworld/MazeIsland.cs index a55458a1d..2fb772e14 100644 --- a/RandomizerCore/Overworld/MazeIsland.cs +++ b/RandomizerCore/Overworld/MazeIsland.cs @@ -75,8 +75,6 @@ .. rom.LoadLocations(LocationID.MI_CHILD_DROP, 1, terrains), childDrop = GetLocation(LocationID.MI_CHILD_DROP); magicContainerDrop = GetLocation(LocationID.MI_MAGIC_CONTAINER_DROP); locationAtPalace4 = GetLocation(LocationID.MI_PALACE4); - locationAtPalace4.PalaceNumber = 4; - locationAtPalace4.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; continentId = Continent.MAZE; baseAddr = RomMap.ContinentLocationBases[continentId]; @@ -111,7 +109,7 @@ .. rom.LoadLocations(LocationID.MI_CHILD_DROP, 1, terrains), trapLocations.Remove(removeLoc); } } - SetVanillaCollectables(props.ReplaceFireWithDash); + //SetVanillaCollectables(props.ReplaceFireWithDash); } public override bool Terraform(RandomizerProperties props, ROM rom) @@ -1004,22 +1002,25 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useDash) { locationAtPalace4.VanillaCollectable = Collectable.BOOTS; childDrop.VanillaCollectable = Collectable.CHILD; magicContainerDrop.VanillaCollectable = Collectable.MAGIC_CONTAINER; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("MAZE ISLAND: "); - sb.AppendLine("\tMagic Container Drop: " + magicContainerDrop.Collectables[0].EnglishText()); - sb.AppendLine("\tChild Drop: " + childDrop.Collectables[0].EnglishText()); + sb.AppendLine("\tMagic Container Drop: " + magicContainerDrop.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tChild Drop: " + childDrop.GetAllCollectables()[0].EnglishText()); - sb.Append("\tPalace 4 (" + locationAtPalace4.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace4.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace4.Collectables.Select(c => c.EnglishText()))); + sb.Append("\tPalace 4 (" + locationAtPalace4.Palace!.Number + "): "); + List palaceCollectables = locationAtPalace4.GetAllCollectables(); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); sb.AppendLine(); return sb.ToString(); @@ -1046,4 +1047,12 @@ public override void DisableDisallowedPassthroughs() magicContainerDrop.IsPassthrough = false; childDrop.IsPassthrough = false; } + + public override void ResetCollectables(RandomizerProperties props) + { + childDrop.SetCollectables([Collectable.CHILD]); + childDrop.CollectablesAreShufflable = props.ShuffleOverworldItems; + magicContainerDrop.SetCollectables([Collectable.MAGIC_CONTAINER]); + magicContainerDrop.CollectablesAreShufflable = props.ShuffleOverworldItems; + } } diff --git a/RandomizerCore/Overworld/WestHyrule.cs b/RandomizerCore/Overworld/WestHyrule.cs index 2d206da32..7ed8cbebe 100644 --- a/RandomizerCore/Overworld/WestHyrule.cs +++ b/RandomizerCore/Overworld/WestHyrule.cs @@ -5,6 +5,7 @@ using System.Text; using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore.Overworld; @@ -13,11 +14,12 @@ public sealed class WestHyrule : World private readonly new Logger logger = LogManager.GetCurrentClassLogger(); public Location northPalace; - public Location locationAtMido; + public Location rauru; + public Location ruto; + public Location sariaNorth; + public Location sariaSouth; + public Location mido; public Location bagu; - public Location mirrorTable; - public Location midoChurch; - public Location locationAtRuto; public Location medicineCave; public Location trophyCave; public Location locationAtPalace1; @@ -26,9 +28,6 @@ public sealed class WestHyrule : World public Location magicContainerCave; public Location grassTile; public Location heartContainerCave; - public Location locationAtSariaNorth; - public Location locationAtSariaSouth; - public Location locationAtRauru; private Location bridge1; private Location bridge2; @@ -121,9 +120,6 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), northPalace = GetLocation(LocationID.WEST_NORTH_PALACE); //0x462f medicineCave = GetLocation(LocationID.WEST_CAVE_MEDICINE); //0x463e - bagu = GetLocation(LocationID.WEST_BAGU_HOUSE); //0x4661 - bagu.ActualTown = Town.BAGU; - bagu.Collectables = [Collectable.BAGUS_NOTE]; trophyCave = GetLocation(LocationID.WEST_CAVE_TROPHY); //0x00004630 magicContainerCave = GetLocation(LocationID.WEST_CAVE_MAGIC_CONTAINER); grassTile = GetLocation(LocationID.WEST_GRASS); @@ -131,46 +127,33 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), pbagCave = GetLocation(LocationID.WEST_CAVE_PBAG); //Towns - locationAtRauru = GetLocation(LocationID.WEST_TOWN_RAURO); - locationAtRauru.Collectables = [Collectable.SHIELD_SPELL]; - locationAtRauru.CollectableRequirements = props.DisableMagicRecs ? Requirements.NONE : new Requirements([RequirementType.ONE_CONTAINER]); - - locationAtRuto = GetLocation(LocationID.WEST_TOWN_RUTO); //0x465e - locationAtRuto.Collectables = [Collectable.JUMP_SPELL]; - locationAtRuto.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.TROPHY]) - : new Requirements([], [[RequirementType.TROPHY, RequirementType.TWO_CONTAINERS]]); - - locationAtSariaNorth = GetLocation(LocationID.WEST_TOWN_SARIA_NORTH); //0x00004660 - locationAtSariaNorth.Collectables = [Collectable.LIFE_SPELL]; - locationAtSariaNorth.ConnectionRequirements = new Requirements([RequirementType.FAIRY, RequirementType.BAGU_LETTER], [[RequirementType.JUMP, RequirementType.DASH]]); - locationAtSariaNorth.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.MIRROR]) - : new Requirements([], [[RequirementType.MIRROR, RequirementType.THREE_CONTAINERS]]); - - locationAtSariaSouth = GetLocation(LocationID.WEST_TOWN_SARIA_SOUTH); //0x0000465f - locationAtSariaSouth.ConnectionRequirements = locationAtSariaNorth.ConnectionRequirements; - - locationAtMido = GetLocation(LocationID.WEST_TOWN_MIDO); //0x00004662 - locationAtMido.Collectables = [Collectable.FAIRY_SPELL]; - locationAtMido.CollectableRequirements = props.DisableMagicRecs ? - new Requirements([RequirementType.MEDICINE]) - : new Requirements([], [[RequirementType.MEDICINE, RequirementType.FOUR_CONTAINERS]]); + bagu = GetLocation(LocationID.WEST_BAGU_HOUSE); //0x4661 + bagu.Town = Towns.LoadVanillaTown(rom, TownType.BAGU, props.DisableMagicRecs); + + rauru = GetLocation(LocationID.WEST_TOWN_RAURO); + rauru.Town = Towns.LoadVanillaTown(rom, TownType.RAURU, props.DisableMagicRecs); + + ruto = GetLocation(LocationID.WEST_TOWN_RUTO); //0x465e + rauru.Town = Towns.LoadVanillaTown(rom, TownType.RUTO, props.DisableMagicRecs); + + sariaNorth = GetLocation(LocationID.WEST_TOWN_SARIA_NORTH); //0x00004660 + Town sariaTown = rauru.Town = Towns.LoadVanillaTown(rom, TownType.SARIA, props.DisableMagicRecs); + sariaNorth.Town = sariaTown; + + sariaSouth = GetLocation(LocationID.WEST_TOWN_SARIA_SOUTH); //0x0000465f + sariaSouth.Town = sariaTown; + + mido = GetLocation(LocationID.WEST_TOWN_MIDO); //0x00004662 + mido.Town = rauru.Town = Towns.LoadVanillaTown(rom, TownType.MIDO, props.DisableMagicRecs); //Palaces locationAtPalace1 = GetLocation(LocationID.WEST_PALACE1); - locationAtPalace1.PalaceNumber = 1; - locationAtPalace1.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; locationAtPalace2 = GetLocation(LocationID.WEST_PALACE2); - locationAtPalace2.PalaceNumber = 2; - locationAtPalace2.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; locationAtPalace3 = GetLocation(LocationID.WEST_PALACE3); - locationAtPalace3.PalaceNumber = 3; - locationAtPalace3.CollectableRequirements = DEFAULT_PALACE_REQUIREMENTS; //Connectors fairyCave = GetLocation(LocationID.WEST_FAIRY_CAVE_DROP); //0x4640 - fairyCave.ConnectionRequirements = new Requirements([RequirementType.FAIRY]); + fairyCave.AccessRequirements = new Requirements([RequirementType.FAIRY]); jumpCave = GetLocation(LocationID.WEST_CAVE_JUMP_NORTH); //0x463b jumpCave.AccessRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); @@ -184,24 +167,6 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), bridge1 = GetLocation(LocationID.WEST_BRIDGE_AFTER_DM_WEST); //0x4644 bridge2 = GetLocation(LocationID.WEST_BRIDGE_AFTER_DM_EAST); //0x4645 - //Fake locations that dont correspond to anywhere on the map, but still hold logic and items - mirrorTable = new Location(locationAtSariaNorth); - mirrorTable.Collectables = [Collectable.MIRROR]; - mirrorTable.ActualTown = Town.SARIA_TABLE; - mirrorTable.Name = "Saria Mirror Table"; - mirrorTable.CanShuffle = false; - locationAtSariaNorth.Children.Add(mirrorTable); - AddLocation(mirrorTable); - - midoChurch = new Location(locationAtMido); - midoChurch.Collectables = [Collectable.DOWNSTAB]; - midoChurch.CollectableRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); - midoChurch.ActualTown = Town.MIDO_CHURCH; - midoChurch.Name = "Mido Church"; - midoChurch.CanShuffle = false; - locationAtMido.Children.Add(midoChurch); - AddLocation(midoChurch); - caveConnections = []; // (left,right) caveConnections.Add((parapaCave1, parapaCave2)); caveConnections.Add((jumpCave, jumpCave2)); @@ -360,7 +325,7 @@ .. rom.LoadLocations(LocationID.WEST_TOWN_RUTO, 8, terrains), GetLocation(LocationID.WEST_BAGU_WOODS5), GetLocation(LocationID.WEST_BAGU_WOODS1) ]; - SetVanillaCollectables(props.ReplaceFireWithDash); + //SetVanillaCollectables(props.ReplaceFireWithDash); } public override bool Terraform(RandomizerProperties props, ROM rom) @@ -430,8 +395,8 @@ public override bool Terraform(RandomizerProperties props, ROM rom) { AllLocations.ForEach(i => i.CanShuffle = true); Terrain riverTerrain = Terrain.MOUNTAIN; - locationAtSariaSouth.CanShuffle = false; - locationAtSariaNorth.CanShuffle = false; + sariaSouth.CanShuffle = false; + sariaNorth.CanShuffle = false; map = new Terrain[MapRows, MapColumns]; @@ -501,8 +466,8 @@ public override bool Terraform(RandomizerProperties props, ROM rom) rows--; } } - locationAtSariaSouth.CanShuffle = false; - locationAtSariaNorth.CanShuffle = false; + sariaSouth.CanShuffle = false; + sariaNorth.CanShuffle = false; walkableTerrains = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE]; //Islands specifically seeds preplaced water instead of normally expandable water to make wetlands less insane randomTerrainFilter = new List { Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, @@ -591,8 +556,8 @@ public override bool Terraform(RandomizerProperties props, ROM rom) rows--; } } - locationAtSariaSouth.CanShuffle = false; - locationAtSariaNorth.CanShuffle = false; + sariaSouth.CanShuffle = false; + sariaNorth.CanShuffle = false; break; default: walkableTerrains = [Terrain.DESERT, Terrain.GRASS, Terrain.FOREST, Terrain.SWAMP, Terrain.GRAVE]; @@ -1397,7 +1362,7 @@ public override void UpdateVisit(List requireables) if (location.AccessRequirements.AreSatisfiedBy(requireables)) { location.Reachable = true; - if (connections.ContainsKey(location) && location.ConnectionRequirements.AreSatisfiedBy(requireables)) + if (connections.ContainsKey(location) && (location.Town == null || location.Town.CanBeTraversed(requireables))) { Location connectedLocation = connections[location]; connectedLocation.Reachable = true; @@ -1488,11 +1453,11 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool HashSet requiredLocations = [ northPalace, - locationAtRauru, - locationAtMido, - locationAtRuto, - locationAtSariaNorth, - locationAtSariaSouth, + rauru, + mido, + ruto, + sariaNorth, + sariaSouth, bagu, medicineCave, @@ -1517,16 +1482,17 @@ public override IEnumerable RequiredLocations(bool hiddenPalace, bool return requiredLocations.Where(i => i != null); } + /* protected override void SetVanillaCollectables(bool useFire) { locationAtPalace1.VanillaCollectable = Collectable.CANDLE; locationAtPalace2.VanillaCollectable = Collectable.GLOVE; locationAtPalace3.VanillaCollectable = Collectable.RAFT; - locationAtRauru.VanillaCollectable = Collectable.SHIELD_SPELL; - locationAtRuto.VanillaCollectable = Collectable.JUMP_SPELL; - locationAtSariaNorth.VanillaCollectable = Collectable.LIFE_SPELL; - locationAtMido.VanillaCollectable = Collectable.FAIRY_SPELL; + rauru.VanillaCollectable = Collectable.SHIELD_SPELL; + ruto.VanillaCollectable = Collectable.JUMP_SPELL; + sariaNorth.VanillaCollectable = Collectable.LIFE_SPELL; + mido.VanillaCollectable = Collectable.FAIRY_SPELL; midoChurch.VanillaCollectable = Collectable.DOWNSTAB; bagu.VanillaCollectable = Collectable.BAGUS_NOTE; mirrorTable.VanillaCollectable = Collectable.MIRROR; @@ -1538,34 +1504,36 @@ protected override void SetVanillaCollectables(bool useFire) trophyCave.VanillaCollectable = Collectable.TROPHY; medicineCave.VanillaCollectable = Collectable.MEDICINE; } + */ public override string GenerateSpoiler() { StringBuilder sb = new(); sb.AppendLine("WEST: "); - sb.AppendLine("\tRauru: " + AllLocations.First(i => i.ActualTown == Town.RAURU).Collectables[0].EnglishText()); - sb.AppendLine("\tRuto: " + AllLocations.First(i => i.ActualTown == Town.RUTO).Collectables[0].EnglishText()); - sb.AppendLine("\tMirror Table: " + mirrorTable.Collectables[0].EnglishText()); - sb.AppendLine("\tSaria: " + AllLocations.First(i => i.ActualTown == Town.SARIA_NORTH).Collectables[0].EnglishText()); - sb.AppendLine("\tDownstab Guy: " + midoChurch.Collectables[0].EnglishText()); - sb.AppendLine("\tMido: " + AllLocations.First(i => i.ActualTown == Town.MIDO_WEST).Collectables[0].EnglishText()); - sb.AppendLine("\tBagu: " + bagu.Collectables[0].EnglishText()); - - sb.AppendLine("\tMagic Container Cave: " + magicContainerCave.Collectables[0].EnglishText()); - sb.AppendLine("\tTrophy Cave: " + trophyCave.Collectables[0].EnglishText()); - sb.AppendLine("\tGrass Tile: " + grassTile.Collectables[0].EnglishText()); - sb.AppendLine("\tHeart Container Cave: " + heartContainerCave.Collectables[0].EnglishText()); - sb.AppendLine("\tPillar Pbag Cave: " + pbagCave.Collectables[0].EnglishText()); - sb.AppendLine("\tMedicine Cave: " + medicineCave.Collectables[0].EnglishText()); - - sb.Append("\tPalace 1 (" + locationAtPalace1.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace1.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace1.Collectables.Select(c => c.EnglishText()))); - - sb.Append("\tPalace 2 (" + locationAtPalace2.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace2.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace2.Collectables.Select(c => c.EnglishText()))); - - sb.Append("\tPalace 3 (" + locationAtPalace3.PalaceNumber + "): "); - sb.AppendLine(locationAtPalace3.Collectables.Count == 0 ? "No Items" : string.Join(", ", locationAtPalace3.Collectables.Select(c => c.EnglishText()))); + sb.AppendLine("Rauru: " + rauru.Town!.GenerateSpoiler()); + sb.AppendLine("Ruto: " + ruto.Town!.GenerateSpoiler()); + sb.AppendLine("Saria: " + sariaNorth.Town!.GenerateSpoiler()); + sb.AppendLine("Mido: " + mido.Town!.GenerateSpoiler()); + sb.AppendLine("Bagu: " + bagu.Town!.GenerateSpoiler()); + + sb.AppendLine("\tMagic Container Cave: " + magicContainerCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tTrophy Cave: " + trophyCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tGrass Tile: " + grassTile.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tHeart Container Cave: " + heartContainerCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tPillar Pbag Cave: " + pbagCave.GetAllCollectables()[0].EnglishText()); + sb.AppendLine("\tMedicine Cave: " + medicineCave.GetAllCollectables()[0].EnglishText()); + + List palaceCollectables = locationAtPalace1.GetAllCollectables(); + sb.Append("\tPalace 1 (" + locationAtPalace1.Palace!.Number + "): "); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); + + palaceCollectables = locationAtPalace2.GetAllCollectables(); + sb.Append("\tPalace 2 (" + locationAtPalace2.Palace!.Number + "): "); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); + + palaceCollectables = locationAtPalace3.GetAllCollectables(); + sb.Append("\tPalace 3 (" + locationAtPalace3.Palace!.Number + "): "); + sb.AppendLine(palaceCollectables.Count == 0 ? "No Items" : string.Join(", ", palaceCollectables.Select(c => c.EnglishText()))); sb.AppendLine(); return sb.ToString(); @@ -1573,10 +1541,6 @@ public override string GenerateSpoiler() protected override void OnUpdateReachableTrigger() { - if (AllLocations.Where(i => i.ActualTown == Town.SARIA_NORTH).FirstOrDefault()?.Reachable ?? false) - { - mirrorTable.Reachable = true; - } } public override void DisableDisallowedPassthroughs() @@ -1602,4 +1566,39 @@ public override void DisableDisallowedPassthroughs() fairyCave.IsPassthrough = false; //fairy cave } + + public override void ResetCollectables(RandomizerProperties props) + { + rauru.Town!.GetWizard()!.Collectable = Collectable.SHIELD_SPELL; + rauru.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + ruto.Town!.GetWizard()!.Collectable = Collectable.JUMP_SPELL; + ruto.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + + sariaNorth.Town!.GetWizard()!.Collectable = Collectable.LIFE_SPELL; + sariaNorth.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + sariaNorth.Town!.GetTownMap(VanillaTownMap.SARIA_TABLE)!.Collectable = Collectable.MIRROR; + sariaNorth.Town!.GetTownMap(VanillaTownMap.SARIA_TABLE)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + + mido.Town!.GetWizard()!.Collectable = Collectable.FAIRY_SPELL; + mido.Town!.GetWizard()!.CollectableIsShufflable = props.IncludeSpellsInShuffle; + mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.Collectable = Collectable.DOWNSTAB; + mido.Town!.GetTownMap(VanillaTownMap.MIDO_TRAINER)!.CollectableIsShufflable = props.IncludeSwordTechsInShuffle; + + bagu.Town!.GetTownMap(VanillaTownMap.BAGU_INDOORS)!.Collectable = Collectable.BAGUS_NOTE; + bagu.Town!.GetTownMap(VanillaTownMap.BAGU_INDOORS)!.CollectableIsShufflable = props.IncludeQuestItemsInShuffle; + + trophyCave.SetCollectables([Collectable.TROPHY]); + trophyCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + medicineCave.SetCollectables([Collectable.MEDICINE]); + medicineCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + heartContainerCave.SetCollectables([Collectable.HEART_CONTAINER]); + heartContainerCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + magicContainerCave.SetCollectables([Collectable.MAGIC_CONTAINER]); + magicContainerCave.CollectablesAreShufflable = props.ShuffleOverworldItems; + pbagCave.SetCollectables([Collectable.LARGE_BAG]); + pbagCave.CollectablesAreShufflable = props.ShuffleOverworldItems && props.PbagItemShuffle; + grassTile.SetCollectables([Collectable.HEART_CONTAINER]); + grassTile.CollectablesAreShufflable = props.ShuffleOverworldItems; + } } diff --git a/RandomizerCore/Overworld/World.cs b/RandomizerCore/Overworld/World.cs index 853d35cc4..7514abab0 100644 --- a/RandomizerCore/Overworld/World.cs +++ b/RandomizerCore/Overworld/World.cs @@ -7,6 +7,7 @@ using System.Text; using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; //using System.Runtime.InteropServices.WindowsRuntime; @@ -56,8 +57,6 @@ public abstract class World private const int MINIMUM_BRIDGE_LENGTH = 2; - public static readonly Requirements DEFAULT_PALACE_REQUIREMENTS = new Requirements([RequirementType.FAIRY, RequirementType.KEY]); - private static readonly Dictionary MAXIMUM_BRIDGE_LENGTH = new() { { Biome.ISLANDS, 10 }, @@ -195,20 +194,6 @@ protected void Swap(Location l1, Location l2) (l2.Xpos, l1.Xpos) = (l1.Xpos, l2.Xpos); (l2.Y, l1.Y) = (l1.Y, l2.Y); (l2.IsPassthrough, l1.IsPassthrough) = (l1.IsPassthrough, l2.IsPassthrough); - - foreach (Location child in l1.Children) - { - child.Xpos = l1.Xpos; - child.Y = l1.Y; - child.IsPassthrough = l1.IsPassthrough; - } - - foreach (Location child in l2.Children) - { - child.Xpos = l2.Xpos; - child.Y = l2.Y; - child.IsPassthrough = l2.IsPassthrough; - } } protected void RemoveLocations(ICollection locationsToRemove) @@ -307,7 +292,7 @@ public void UpdateAllReached() //TODO: Doing this off terrain type is a very bad idea for stuff like vanilla shuffle no actual terrain. if (location.TerrainType == Terrain.PALACE || location.TerrainType == Terrain.TOWN - || location.Collectables.Any(i => !i.IsInternalUse())) + || location.GetAllCollectables().Any(i => !i.IsInternalUse())) { if (!location.Reachable) { @@ -440,8 +425,7 @@ protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves, Location? hi location.Y = y; location.CanShuffle = false; } - else if (location.TerrainType != Terrain.TOWN || - (location.ActualTown > 0 && (location?.ActualTown?.AppearsOnMap() ?? false))) + else if (location.Town == null) { Terrain t; do @@ -461,15 +445,6 @@ protected bool PlaceLocations(Terrain riverTerrain, bool saneCaves, Location? hi location.CanShuffle = false; } } - - if(location!.TerrainType == Terrain.TOWN) - { - foreach (Location linkedLocation in AllLocations.Where( - loc => !loc.AppearsOnMap && loc.ActualTown?.GetMasterTown() == location.ActualTown)) - { - linkedLocation.Pos = location.Pos; - } - } } return true; } @@ -758,9 +733,7 @@ protected bool ConnectIslands(int maxBridges, bool placeSaria, Terrain riverTerr //Any of the bridge locations that are being placed need to be reset so their vanilla locations aren't avoided. if(placeSaria) { - foreach (Location location in AllLocations.Where(i => i.ActualTown == Town.SARIA_NORTH - || i.ActualTown == Town.SARIA_SOUTH - || i.ActualTown == Town.SARIA_TABLE)) + foreach (Location location in AllLocations.Where(i => i.Town?.Type == TownType.SARIA)) { location.Xpos = 0; location.Y = 0; @@ -1960,6 +1933,8 @@ public void ResetVisitabilityState() } } + public abstract void ResetCollectables(RandomizerProperties props); + protected bool DrawBridge(Direction direction) { return DrawBridge(RNG, map, bridge, walkableTerrains, direction); @@ -3183,42 +3158,11 @@ public bool ValidateCaves() return true; } - /* - public bool PassthroughsIntersectRaftCoordinates(IEnumerable<(int, int)> raftCoordinates) - { - foreach (Location location in AllLocations.Where(i => i.PassThrough != 0)) - { - if (raftCoordinates.Any(i => location.Xpos == i.Item2 && location.YRaw == i.Item1)) - { - return true; - } - } - return false; - } - */ - - //Short term fix, right now linked locations aren't shuffled, and they are counted as reachable when - //their parent location is reachable, but the original location remains in the location list, creating - //a phantom reachability spot at the vanilla location. - //What _should_ happen is there should be a unified interface for updating locations that automicatically - //updates the coordinates, map, linked locations, etc. But that's more work than I want to do, so here's this lazy hack - public void SynchronizeLinkedLocations() - { - foreach (Location parent in AllLocations) - { - foreach (Location child in parent.Children) - { - child.Xpos = parent.Xpos; - child.Y = parent.Y; - } - } - } - public abstract void UpdateVisit(List requireables); public abstract IEnumerable RequiredLocations(bool hiddenPalace, bool hiddenKasuto); - protected abstract void SetVanillaCollectables(bool useDash); + //protected abstract void SetVanillaCollectables(bool useDash); public abstract string GenerateSpoiler(); } \ No newline at end of file diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index fd3becaff..23ead07ff 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -1,15 +1,18 @@ -using System; +using js65; +using NLog; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; -using js65; -using NLog; +using System.Xml.Linq; using Z2Randomizer.RandomizerCore.Enemy; using Z2Randomizer.RandomizerCore.Overworld; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -156,6 +159,8 @@ public class ROM public static readonly int[] ZeldaFacePaletteAddr = { 0x4023, 0x8023, 0x14047, 0x140c9 }; public static readonly int[] ZeldaDressPaletteAddr = { 0x4024, 0x8024, 0x14048, 0x140c8 }; + public static readonly byte[] DOOR_CONNECTION_BLACKLIST = [0x00, 0xFC, 0xFD, 0xFE, 0xFF]; + public byte[] rawdata { get; } public ROM(string filename, bool expandRom = false) @@ -2496,6 +2501,52 @@ public Location LoadLocation(LocationID lid, Terrain terrain, bool? passthroughO }; } + public Town LoadTown(int startMapNum, int numMaps) + { + List townMaps = []; + + for (int currentMapNum = startMapNum; currentMapNum - startMapNum < numMaps; currentMapNum++) + { + townMaps.Add(LoadTownMap(currentMapNum, false)); + + var roomDoorConnectionsRomAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x8817 + currentMapNum * 4); + var roomDoorConnections = GetBytes(roomDoorConnectionsRomAddr, 4); + + for(int connectionNumber = 0; connectionNumber < 4; connectionNumber++) + { + byte internalMapNumber = (byte)(roomDoorConnections[connectionNumber] & 0xFC >> 2); + if(!DOOR_CONNECTION_BLACKLIST.Contains(internalMapNumber)) + { + townMaps.Add(LoadTownMap(internalMapNumber, true)); + } + } + } + + var romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x871B + startMapNum * 4); + var connectionsRaw = GetBytes(romAddr, 4 * numMaps); + romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x8817 + startMapNum * 4); + var doorConnectionsRaw = GetBytes(romAddr, 4 * numMaps); + + return new Town(townMaps, connectionsRaw, doorConnectionsRaw); + } + + public TownMap LoadTownMap(int currentMapNum, bool isInternalMap) + { + var romPtr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x8523 + 2 * currentMapNum); + var nesAddr = GetShort(romPtr + 1, romPtr); + var romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, nesAddr); + var length = GetByte(romAddr); + var sideviewRaw = GetBytes(romAddr, length); + + romPtr = NesPointer.ConvertNesPtrToPrgRomAddr(3, 0x85a1 + 2 * currentMapNum); + nesAddr = GetShort(romPtr + 1, romPtr); + romAddr = NesPointer.ConvertNesPtrToPrgRomAddr(3, nesAddr); + length = GetByte(romAddr); + byte[] enemiesRaw = GetBytes(romAddr, length); + + return new TownMap(currentMapNum, sideviewRaw, enemiesRaw, isInternalMap); + } + public void RemoveUnusedConnectors(World world) { if (world.raft == null) @@ -2520,8 +2571,7 @@ public void RemoveUnusedConnectors(World world) } //This was refactored out of EastHyrule. The signature/timing/structure needs work. - public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, Location hiddenPalaceLocation, - Location townAtNewKasuto, Location spellTower, bool vanillaShuffleUsesActualTerrain) + public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, Location hiddenPalaceLocation, bool vanillaShuffleUsesActualTerrain) { if (!biome.UsesVanillaMap()) { @@ -2535,10 +2585,6 @@ public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, L Put(0x1ccc0, (byte)pos); int connection = hiddenPalaceLocation.MemAddress - 0x862F; Put(0x1df76, (byte)connection); - if (hiddenPalaceLocation == spellTower) - { - throw new Exception("Child locations shouldn't be able to be hidden spots"); - } if (vanillaShuffleUsesActualTerrain || biome != Biome.VANILLA_SHUFFLE) { Put(0x1df74, (byte)hiddenPalaceLocation.TerrainType); @@ -2647,7 +2693,7 @@ public void UpdateHiddenPalaceSpot(Biome biome, (int, int) hiddenPalaceCoords, L } - public void UpdateKasuto(Location hiddenKasutoLocation, Location townAtNewKasuto, Location spellTower, Biome biome, + public void UpdateKasuto(Location hiddenKasutoLocation, Biome biome, int baseAddr, Terrain hiddenKasutoTerrain, bool vanillaShuffleUsesActualTerrain) { Put(0x1df79, (byte)(hiddenKasutoLocation.YRaw + (hiddenKasutoLocation.IsExternalWorld ? 0x80 : 0))); @@ -2657,10 +2703,6 @@ public void UpdateKasuto(Location hiddenKasutoLocation, Location townAtNewKasuto Put(0x1ccdb, (byte)hiddenKasutoLocation.YRaw); int connection = hiddenKasutoLocation.MemAddress - baseAddr; Put(0x1df77, (byte)connection); - if (hiddenKasutoLocation == spellTower) - { - throw new Exception("Child locations shouldn't be able to be hidden spots"); - } if (vanillaShuffleUsesActualTerrain || biome != Biome.VANILLA_SHUFFLE) { //Terrain t = terrains[hiddenKasutoLocation.MemAddress]; diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 25b26c2ac..bfeffc93f 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -9,7 +9,7 @@ using System.Runtime.CompilerServices; using NLog; using Z2Randomizer.RandomizerCore.Flags; -using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; diff --git a/RandomizerCore/RandomizerProperties.cs b/RandomizerCore/RandomizerProperties.cs index 8a19ca006..cb2d166e8 100644 --- a/RandomizerCore/RandomizerProperties.cs +++ b/RandomizerCore/RandomizerProperties.cs @@ -246,7 +246,7 @@ public class RandomizerProperties [Key] public int Id { get; set; } - public bool StartsWithCollectable(Collectable collectable) + public bool StartsWithCollectable(Collectable? collectable) { return collectable switch { diff --git a/RandomizerCore/RequirementType.cs b/RandomizerCore/RequirementType.cs index 1a515eb2d..4b9109a63 100644 --- a/RandomizerCore/RequirementType.cs +++ b/RandomizerCore/RequirementType.cs @@ -1,4 +1,5 @@  +using System; using System.Collections.Generic; namespace Z2Randomizer.RandomizerCore; @@ -35,6 +36,7 @@ public enum RequirementType public static class RequirementTypeExtensions { + public static readonly List ALL = [..Enum.GetValues()]; /// Mirror of Collectable.AsRequirement() public static Collectable? AsCollectable(this RequirementType requirementType) { diff --git a/RandomizerCore/Requirements.cs b/RandomizerCore/Requirements.cs index 54534f6d1..7a2a1e65e 100644 --- a/RandomizerCore/Requirements.cs +++ b/RandomizerCore/Requirements.cs @@ -6,12 +6,15 @@ using System.Text.Json; using System.Text.Json.Serialization; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; public class Requirements { public static readonly Requirements NONE = new(); + public static readonly Requirements DEFAULT_PALACE_REQUIREMENTS = new([RequirementType.FAIRY, RequirementType.KEY]); + private static readonly Dictionary ImplicitRequirements = new() { diff --git a/RandomizerCore/RomMap.cs b/RandomizerCore/RomMap.cs index c653641fe..79f6e831f 100644 --- a/RandomizerCore/RomMap.cs +++ b/RandomizerCore/RomMap.cs @@ -134,4 +134,5 @@ class RomMap public const int PALACE_PALETTE_TABLE_ENTRANCES = 0x10480; public const int PALACE_PALETTE_TABLE_PER_PALACE = 0x13f10; public const int GP_PALETTE_TABLE_MAJOR = 0x1401e; + } diff --git a/RandomizerCore/Shuffler.cs b/RandomizerCore/Shuffler.cs index 7f70b4d3a..e545e9871 100644 --- a/RandomizerCore/Shuffler.cs +++ b/RandomizerCore/Shuffler.cs @@ -3,7 +3,7 @@ using System.Linq; using js65; using NLog; -using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; diff --git a/RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/ByEntranceDirectionItemRoomSelectionStrategy.cs similarity index 96% rename from RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/ByEntranceDirectionItemRoomSelectionStrategy.cs index 2378a7e6c..6fbf29b9e 100644 --- a/RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/ByEntranceDirectionItemRoomSelectionStrategy.cs @@ -4,7 +4,7 @@ using System.Linq; using Z2Randomizer.RandomizerCore; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class ByEntranceDirectionItemRoomSelectionStrategy : ItemRoomSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/ByShapeItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/ByShapeItemRoomSelectionStrategy.cs similarity index 99% rename from RandomizerCore/Sidescroll/ByShapeItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/ByShapeItemRoomSelectionStrategy.cs index e762d58f5..72daa27e2 100644 --- a/RandomizerCore/Sidescroll/ByShapeItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/ByShapeItemRoomSelectionStrategy.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class ByShapeItemRoomSelectionStrategy : ItemRoomSelectionStrategy, IItemRoomInShapeSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ChaosPalaceGenerator.cs similarity index 98% rename from RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ChaosPalaceGenerator.cs index 57c6545ce..429f87804 100644 --- a/RandomizerCore/Sidescroll/ChaosPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ChaosPalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class ChaosPalaceGenerator : PalaceGenerator { protected static readonly Logger logger = LogManager.GetCurrentClassLogger(); @@ -18,7 +18,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, debug++; bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); RoomPool roomPool = new(rooms); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); var palaceGroup = Util.AsPalaceGrouping(palaceNumber); palace.Entrance = new(roomPool.Entrances[r.Next(roomPool.Entrances.Count)]) diff --git a/RandomizerCore/Sidescroll/CoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/CoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/CoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/CoordinatePalaceGenerator.cs index 0d615b8b8..7a4e675c4 100644 --- a/RandomizerCore/Sidescroll/CoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/CoordinatePalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Linq; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class CoordinatePalaceGenerator : PalaceGenerator { diff --git a/RandomizerCore/Sidescroll/ItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/ItemRoomSelectionStrategy.cs similarity index 90% rename from RandomizerCore/Sidescroll/ItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/ItemRoomSelectionStrategy.cs index e9cb8eeda..d54a452a9 100644 --- a/RandomizerCore/Sidescroll/ItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/ItemRoomSelectionStrategy.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class ItemRoomSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/Palace.cs b/RandomizerCore/Sidescroll/Palace/Palace.cs similarity index 97% rename from RandomizerCore/Sidescroll/Palace.cs rename to RandomizerCore/Sidescroll/Palace/Palace.cs index 26647f65b..ddddba38a 100644 --- a/RandomizerCore/Sidescroll/Palace.cs +++ b/RandomizerCore/Sidescroll/Palace/Palace.cs @@ -1,24 +1,25 @@ -using System; +using NLog; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using NLog; using Z2Randomizer.RandomizerCore.Enemy; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; using static Z2Randomizer.RandomizerCore.Util; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; -public partial class Palace +public partial class Palace(int number, bool palaceItemsAreShufflable) { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private const double DROPS_CAN_LEAD_TO_BOSS_CHANCE = 0.4; //private const bool DROPS_ARE_BLOCKERS = false; private const byte OUTSIDE_ROOM_EXIT = 0b11111100; - private readonly SortedDictionary> rooms; + private readonly SortedDictionary> rooms = []; internal bool IsValid { get; set; } = false; public static readonly ReadOnlyCollection SHUFFLABLE_SMALL_ITEMS = [ @@ -32,12 +33,13 @@ public partial class Palace Collectable.ONEUP ]; - public List AllRooms { get; private set; } + public List AllRooms { get; private set; } = []; - public Room? Entrance { get; set; } - public List ItemRooms { get; set; } + public Room? Entrance { get; set; } = null; + public List ItemRooms { get; set; } = []; + public bool PalaceItemsAreShufflable { get; set; } = palaceItemsAreShufflable; public Room? BossRoom { get; set; } - public int Number { get; set; } + public int Number { get; set; } = number; internal Room? TbirdRoom { get; set; } //DEBUG @@ -45,22 +47,6 @@ public partial class Palace public PalaceGrouping PalaceGroup => Util.AsPalaceGrouping(Number) ?? throw new Exception("Palace number out of range"); - public Palace(int number) - { - Number = number; - Entrance = null; - rooms = []; - AllRooms = []; - ItemRooms = []; - } - - /* - public void UpdateRomItem(Collectable collectable, ROM ROMData) - { - ItemRoom?.UpdateRomItem(collectable, ROMData); - } - */ - public bool RequiresThunderbird() { CheckSpecialPaths(Entrance!); @@ -961,40 +947,6 @@ public void RandomizeSmallItems(Random r, bool extraKeys) } } } - - /* - public List CheckBlocks() - { - return CheckBlocksHelper([], [], Entrance!); - } - - private List CheckBlocksHelper(List c, List blockers, Room r) - { - if (c.Contains(ItemRoom!)) - { - return c; - } - c.Add(r); - if (r.Up != null && !c.Contains(r.Up)) - { - CheckBlocksHelper(c, blockers, r.Up); - } - if (r.Down != null && !c.Contains(r.Down)) - { - CheckBlocksHelper(c, blockers, r.Down); - } - if (r.Left != null && !c.Contains(r.Left)) - { - CheckBlocksHelper(c, blockers, r.Left); - } - if (r.Right != null && !c.Contains(r.Right)) - { - CheckBlocksHelper(c, blockers, r.Right); - } - return c; - } - */ - public void ResetRooms() { foreach (Room r in AllRooms) @@ -1175,8 +1127,12 @@ public bool CanReachAnItemRoom(IEnumerable requireables) return false; } - public List GetGettableItems(IEnumerable initialRequireables) + public List GetGettableItems(IEnumerable initialRequireables, bool shufflableItemsOnly) { + if(shufflableItemsOnly && !PalaceItemsAreShufflable) + { + return []; + } List requireables = []; requireables.AddRange(initialRequireables); List pendingRooms = new() { AllRooms.First(i => i.IsEntrance) }; @@ -1235,6 +1191,27 @@ public List GetGettableItems(IEnumerable initialRe return gettableItems; } + public void SetCollectables(IEnumerable collectables) + { + Debug.Assert(collectables.Count() == ItemRooms.Count); + int i = 0; + foreach (Collectable collectable in collectables) + { + ItemRooms[i++].Collectable = collectable; + } + } + + public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) + { + Room? room = ItemRooms.FirstOrDefault(i => i.Collectable == toReplace); + if (room == null) + { + return false; + } + room.Collectable = replacement; + return true; + } + [Conditional("DEBUG")] public void AssertItemRoomsAreUnique(ROM rom) { diff --git a/RandomizerCore/Sidescroll/PalaceColors.cs b/RandomizerCore/Sidescroll/Palace/PalaceColors.cs similarity index 99% rename from RandomizerCore/Sidescroll/PalaceColors.cs rename to RandomizerCore/Sidescroll/Palace/PalaceColors.cs index 922a87286..91dc9437c 100644 --- a/RandomizerCore/Sidescroll/PalaceColors.cs +++ b/RandomizerCore/Sidescroll/Palace/PalaceColors.cs @@ -1,5 +1,4 @@ - -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; /// /// Color combinations that may be chosen for palace bricks and diff --git a/RandomizerCore/Sidescroll/PalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/PalaceGenerator.cs similarity index 98% rename from RandomizerCore/Sidescroll/PalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/PalaceGenerator.cs index 707ba44c2..153c21aab 100644 --- a/RandomizerCore/Sidescroll/PalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/PalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Numerics; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class PalaceGenerator { diff --git a/RandomizerCore/Sidescroll/PalaceRooms.cs b/RandomizerCore/Sidescroll/Palace/PalaceRooms.cs similarity index 99% rename from RandomizerCore/Sidescroll/PalaceRooms.cs rename to RandomizerCore/Sidescroll/Palace/PalaceRooms.cs index 1f3b3de5c..5a30791cf 100644 --- a/RandomizerCore/Sidescroll/PalaceRooms.cs +++ b/RandomizerCore/Sidescroll/Palace/PalaceRooms.cs @@ -5,7 +5,7 @@ using System.Text.Json; using System.Text.RegularExpressions; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public partial class PalaceRooms { diff --git a/RandomizerCore/Sidescroll/Palaces.cs b/RandomizerCore/Sidescroll/Palace/Palaces.cs similarity index 99% rename from RandomizerCore/Sidescroll/Palaces.cs rename to RandomizerCore/Sidescroll/Palace/Palaces.cs index 91de75a27..54516b7c9 100644 --- a/RandomizerCore/Sidescroll/Palaces.cs +++ b/RandomizerCore/Sidescroll/Palace/Palaces.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public enum PalaceGrouping { @@ -156,6 +156,7 @@ public async Task> CreatePalaces(Random r, RandomizerProperties pro } } } + return palaces; } diff --git a/RandomizerCore/Sidescroll/RandomItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/Palace/RandomItemRoomSelectionStrategy.cs similarity index 98% rename from RandomizerCore/Sidescroll/RandomItemRoomSelectionStrategy.cs rename to RandomizerCore/Sidescroll/Palace/RandomItemRoomSelectionStrategy.cs index fd2a86d61..f3c4baa9d 100644 --- a/RandomizerCore/Sidescroll/RandomItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/Palace/RandomItemRoomSelectionStrategy.cs @@ -3,7 +3,7 @@ using System.Linq; using YamlDotNet.Core.Tokens; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class RandomItemRoomSelectionStrategy : ItemRoomSelectionStrategy { diff --git a/RandomizerCore/Sidescroll/RandomWalkCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/RandomWalkCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/RandomWalkCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/RandomWalkCoordinatePalaceGenerator.cs index e5f522994..bea281eca 100644 --- a/RandomizerCore/Sidescroll/RandomWalkCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/RandomWalkCoordinatePalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class RandomWalkCoordinatePalaceGenerator : ShapeFirstCoordinatePalaceGenerator { diff --git a/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ReconstructedLoopyPalaceGenerator.cs similarity index 96% rename from RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ReconstructedLoopyPalaceGenerator.cs index 16fc9749e..1f34987a9 100644 --- a/RandomizerCore/Sidescroll/ReconstructedLoopyPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ReconstructedLoopyPalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class ReconstructedLoopyPalaceGenerator(CancellationToken ct) : ReconstructedPalaceGenerator(ct) { diff --git a/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ReconstructedPalaceGenerator.cs similarity index 98% rename from RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ReconstructedPalaceGenerator.cs index 4ad33d683..0880985c9 100644 --- a/RandomizerCore/Sidescroll/ReconstructedPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ReconstructedPalaceGenerator.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class ReconstructedPalaceGenerator(CancellationToken ct) : PalaceGenerator { @@ -21,7 +21,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, debug++; bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); // var palaceGroup = Util.AsPalaceGrouping(palaceNumber); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); do // while (tries >= PALACE_SHUFFLE_ATTEMPT_LIMIT); { await Task.Yield(); @@ -37,7 +37,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, //No longer a loop since the room pool can't (and would need to be) regenerated after each attempt. //Now we just fail and restart if we can't place enough rooms - palace = new(palaceNumber); + palace = new(palaceNumber, props.ShufflePalaceItems); palace.Entrance = new(roomPool.Entrances[r.Next(roomPool.Entrances.Count)]) { IsRoot = true, diff --git a/RandomizerCore/Sidescroll/Room.cs b/RandomizerCore/Sidescroll/Palace/Room.cs similarity index 99% rename from RandomizerCore/Sidescroll/Room.cs rename to RandomizerCore/Sidescroll/Palace/Room.cs index dd6fce0ec..58603c808 100644 --- a/RandomizerCore/Sidescroll/Room.cs +++ b/RandomizerCore/Sidescroll/Palace/Room.cs @@ -8,7 +8,7 @@ using js65; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public record struct Coord(int X, int Y) : IComparable { diff --git a/RandomizerCore/Sidescroll/RoomExitType.cs b/RandomizerCore/Sidescroll/Palace/RoomExitType.cs similarity index 98% rename from RandomizerCore/Sidescroll/RoomExitType.cs rename to RandomizerCore/Sidescroll/Palace/RoomExitType.cs index 5abc96c52..93718562d 100644 --- a/RandomizerCore/Sidescroll/RoomExitType.cs +++ b/RandomizerCore/Sidescroll/Palace/RoomExitType.cs @@ -1,7 +1,7 @@ using System; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public enum RoomExitType { diff --git a/RandomizerCore/Sidescroll/Palace/RoomGroup.cs b/RandomizerCore/Sidescroll/Palace/RoomGroup.cs new file mode 100644 index 000000000..44769229c --- /dev/null +++ b/RandomizerCore/Sidescroll/Palace/RoomGroup.cs @@ -0,0 +1,6 @@ +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; + +public enum RoomGroup +{ + VANILLA, V4_0, V5_0, STUBS +} diff --git a/RandomizerCore/Sidescroll/RoomPool.cs b/RandomizerCore/Sidescroll/Palace/RoomPool.cs similarity index 99% rename from RandomizerCore/Sidescroll/RoomPool.cs rename to RandomizerCore/Sidescroll/Palace/RoomPool.cs index d94e23eff..99e25ac2e 100644 --- a/RandomizerCore/Sidescroll/RoomPool.cs +++ b/RandomizerCore/Sidescroll/Palace/RoomPool.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class RoomPool { diff --git a/RandomizerCore/Sidescroll/SegmentConnectionType.cs b/RandomizerCore/Sidescroll/Palace/SegmentConnectionType.cs similarity index 95% rename from RandomizerCore/Sidescroll/SegmentConnectionType.cs rename to RandomizerCore/Sidescroll/Palace/SegmentConnectionType.cs index af0f9c4f1..2f6915966 100644 --- a/RandomizerCore/Sidescroll/SegmentConnectionType.cs +++ b/RandomizerCore/Sidescroll/Palace/SegmentConnectionType.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public enum SegmentConnectionType { diff --git a/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/SequentialPlacementCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/SequentialPlacementCoordinatePalaceGenerator.cs index 2a4ee2304..7bd8008b8 100644 --- a/RandomizerCore/Sidescroll/SequentialPlacementCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/SequentialPlacementCoordinatePalaceGenerator.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class SequentialPlacementCoordinatePalaceGenerator : CoordinatePalaceGenerator { @@ -33,7 +33,7 @@ internal override async Task GeneratePalace(RandomizerProperties props, debug++; bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); openCoords = new(); Dictionary> roomsByExitType; roomPool = new(rooms); diff --git a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/ShapeFirstCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/ShapeFirstCoordinatePalaceGenerator.cs index 8f9ea26c9..0611215f8 100644 --- a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/ShapeFirstCoordinatePalaceGenerator.cs @@ -8,7 +8,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public abstract class ShapeFirstCoordinatePalaceGenerator() : CoordinatePalaceGenerator() { @@ -16,7 +16,7 @@ public abstract class ShapeFirstCoordinatePalaceGenerator() : CoordinatePalaceGe internal override async Task GeneratePalace(RandomizerProperties props, RoomPool rooms, Random r, int roomCount, int palaceNumber) { bool duplicateProtection = (props.NoDuplicateRooms || props.NoDuplicateRoomsBySideview) && AllowDuplicatePrevention(props, palaceNumber); - Palace palace = new(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); RoomPool roomPool = new(rooms); var itemRoomSelector = GetItemRoomSelectionStrategy(); // var palaceGroup = Util.AsPalaceGrouping(palaceNumber); diff --git a/RandomizerCore/Sidescroll/TowerCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/TowerCoordinatePalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/TowerCoordinatePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/TowerCoordinatePalaceGenerator.cs index 61affa3b4..528ada4a4 100644 --- a/RandomizerCore/Sidescroll/TowerCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/TowerCoordinatePalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class TowerCoordinatePalaceGenerator : ShapeFirstCoordinatePalaceGenerator { diff --git a/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/VanillaPalaceGenerator.cs similarity index 94% rename from RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/VanillaPalaceGenerator.cs index 113f9b6e1..e4d393293 100644 --- a/RandomizerCore/Sidescroll/VanillaPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaPalaceGenerator.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using NLog; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class VanillaPalaceGenerator() : PalaceGenerator { @@ -18,14 +18,10 @@ internal override async Task GeneratePalace(RandomizerProperties props, { throw new Exception("Invalid vanilla palace room pool"); } - Palace palace = new(palaceNumber); - - // var palaceGroup = Util.AsPalaceGrouping(palaceNumber); + Palace palace = new(palaceNumber, props.ShufflePalaceItems); palace.Entrance = new(roomPool.Entrances.First()); - // palace.Entrance.PalaceGroup = palaceGroup; palace.BossRoom = new(roomPool.BossRooms.First()); - // palace.BossRoom.PalaceGroup = palaceGroup; palace.AllRooms.Add(palace.Entrance); palace.ItemRooms = []; diff --git a/RandomizerCore/Sidescroll/VanillaRoomPool.cs b/RandomizerCore/Sidescroll/Palace/VanillaRoomPool.cs similarity index 97% rename from RandomizerCore/Sidescroll/VanillaRoomPool.cs rename to RandomizerCore/Sidescroll/Palace/VanillaRoomPool.cs index d7ae45881..9aee72ea3 100644 --- a/RandomizerCore/Sidescroll/VanillaRoomPool.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaRoomPool.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; internal class VanillaRoomPool : RoomPool { diff --git a/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/VanillaShufflePalaceGenerator.cs similarity index 95% rename from RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/VanillaShufflePalaceGenerator.cs index 6e192bc1e..bd451f02c 100644 --- a/RandomizerCore/Sidescroll/VanillaShufflePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaShufflePalaceGenerator.cs @@ -1,7 +1,7 @@ using System; using System.Threading.Tasks; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class VanillaShufflePalaceGenerator() : VanillaPalaceGenerator() { diff --git a/RandomizerCore/Sidescroll/VanillaWeightedPalaceGenerator.cs b/RandomizerCore/Sidescroll/Palace/VanillaWeightedPalaceGenerator.cs similarity index 99% rename from RandomizerCore/Sidescroll/VanillaWeightedPalaceGenerator.cs rename to RandomizerCore/Sidescroll/Palace/VanillaWeightedPalaceGenerator.cs index 59f253e20..2f7240269 100644 --- a/RandomizerCore/Sidescroll/VanillaWeightedPalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/Palace/VanillaWeightedPalaceGenerator.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Z2Randomizer.RandomizerCore.Sidescroll; +namespace Z2Randomizer.RandomizerCore.Sidescroll.Palace; public class VanillaWeightedPalaceGenerator : RandomWalkCoordinatePalaceGenerator { diff --git a/RandomizerCore/Sidescroll/RoomGroup.cs b/RandomizerCore/Sidescroll/RoomGroup.cs deleted file mode 100644 index d45f44752..000000000 --- a/RandomizerCore/Sidescroll/RoomGroup.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Z2Randomizer.RandomizerCore.Sidescroll; - -public enum RoomGroup -{ - VANILLA, V4_0, V5_0, STUBS -} diff --git a/RandomizerCore/Sidescroll/SideviewEnums.cs b/RandomizerCore/Sidescroll/SideviewEnums.cs index fd22bb8e9..a4c5e1a1d 100644 --- a/RandomizerCore/Sidescroll/SideviewEnums.cs +++ b/RandomizerCore/Sidescroll/SideviewEnums.cs @@ -177,6 +177,39 @@ public enum GreatPalaceObject PILLAR = 0xF0 } +public enum TownObject +{ + CLOSED_DOOR = 0x00, + OPEN_DOOR1 = 0x01, + OPEN_DOOR2 = 0x02, + OPEN_DOOR3 = 0x03, + STONEHENGE = 0x04, //??? + CHAIR = 0x05, + LARGE_CLOUD = 0x06, + SMALL_CLOUD_07 = 0x07, + TABLE = 0x08, + BENCH = 0x09, + CROSS = 0x0A, + CHIMNEY_ENTRANCE = 0x0B, + CHIMNEY_NON_ENTRANCE = 0x0C, + CHURCH_CROSS = 0x0D, + SMALL_CLOUD_OE = 0x0E, + COLLECTABLE = 0x0F, + CROSS_HASH_ROOF = 0x20, + GREY_ROOF = 0x30, + GREEN_ROOF = 0x40, + BRICK_BUILDING = 0x50, + WHITE_BRICK_BUILDING = 0x60, + GRASS_COLOR_BUILDING = 0x70, + WHITE_WALL = 0x80, + LOG_BUILDING = 0x90, + SQUARE_WINDOW = 0xA0, + GRAY_WINDOW = 0xB0, + BROWN_WINDOWS = 0xC0, + UNDERGROUND_BRICKS = 0xD0, + PILLAR = 0xE0, +} + public static class ForestObjectExtensions { public static int Width(SideviewMapCommand command) diff --git a/RandomizerCore/Sidescroll/Town/Town.cs b/RandomizerCore/Sidescroll/Town/Town.cs new file mode 100644 index 000000000..9506594b5 --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/Town.cs @@ -0,0 +1,195 @@ +using DynamicData; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.MemoryMappedFiles; +using System.Linq; +using System.Text; +using Z2Randomizer.RandomizerCore.Enemy; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +public class Town +{ + + public string Name { get; set; } = ""; + public TownType? Type { get; set; } + + public List TownMaps { get; } = []; + + //When custom towns happen, make this real + //public Town(string townJson) + + public Town(List townMaps, byte[] connectionsRaw, byte[] doorConnections) + { + Debug.Assert(connectionsRaw.Length == doorConnections.Length); + TownMaps = townMaps; + int currentConnectionIndex = 0; + int currentDoorConnectionIndex = 0; + foreach (TownMap townMap in townMaps.OrderBy(i => i.Map)) + { + Debug.Assert(currentConnectionIndex < connectionsRaw.Length); + byte connectionMap = (byte)(connectionsRaw[currentConnectionIndex] & 0xFC << 2); + townMap.Left = townMaps.FirstOrDefault(i => i.Map == connectionMap); + currentConnectionIndex += 3; + connectionMap = (byte)(connectionsRaw[currentConnectionIndex] & 0xFC << 2); + townMap.Right = townMaps.FirstOrDefault(i => i.Map == connectionMap); + + Debug.Assert(currentDoorConnectionIndex < doorConnections.Length); + connectionMap = (byte)(doorConnections[currentDoorConnectionIndex++] & 0xFC << 2); + townMap.Door0 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + connectionMap = (byte)(doorConnections[currentDoorConnectionIndex++] & 0xFC << 2); + townMap.Door1 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + connectionMap = (byte)(doorConnections[currentDoorConnectionIndex++] & 0xFC << 2); + townMap.Door2 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + connectionMap = (byte)(doorConnections[currentDoorConnectionIndex++] & 0xFC << 2); + townMap.Door3 = townMaps.FirstOrDefault(i => i.Map == connectionMap); + } + } + + //Determine what items you can get from this town with these reqireables from this entrance direction + public List GetGettableItems(Direction entranceDirection, List requireables, bool shufflableItemsOnly = false) + { + TownMap startMap = entranceDirection switch + { + Direction.WEST => TownMaps.First(i => i.Left == null), + Direction.EAST => TownMaps.First(i => i.Right == null), + _ => throw new ArgumentException("Invalid entrance direction. Towns can only enter from the west/east") + }; + + List collectables = []; + + List checkedMaps = []; + List mapsToCheck = []; + TownMap currentMap = startMap; + while(currentMap != null) + { + if (currentMap.AccessRequirements.AreSatisfiedBy(requireables)) + { + if (currentMap.Door0 != null) + { + mapsToCheck.Add(currentMap.Door0); + } + if (currentMap.Door1 != null) + { + mapsToCheck.Add(currentMap.Door1); + } + if (currentMap.Door2 != null) + { + mapsToCheck.Add(currentMap.Door2); + } + if (currentMap.Door3 != null) + { + mapsToCheck.Add(currentMap.Door3); + } + if (entranceDirection == Direction.WEST && !currentMap.IsInternalLocation && currentMap.Right != null) + { + mapsToCheck.Add(currentMap.Right); + } + if (entranceDirection == Direction.EAST && !currentMap.IsInternalLocation && currentMap.Left != null) + { + mapsToCheck.Add(currentMap.Left); + } + if (currentMap.Collectable != null && currentMap.CollectableIsShufflable || !shufflableItemsOnly) + { + collectables.Add((Collectable)currentMap.Collectable!); + } + } + mapsToCheck.Remove(currentMap); + currentMap = mapsToCheck[0]; + } + return collectables; + } + + public bool CanBeTraversed(List requireables) + { + TownMap leftMap = TownMaps.First(i => i.Left == null); + + //This currently only checks left-right because all towns are either bi-directionally traversable or oneway deadending right (new kasuto) + //This means it will perform weirdly if you somehow create a monodirectional town entered from the right that you also + //for some reason set as a connection. Don't do that. + List coveredMaps = []; + TownMap currentMap = leftMap; + while(coveredMaps.Count < 63) + { + if(coveredMaps.Contains(currentMap)) + { + throw new Exception($"Loop detected in town connection routing. Check your town connections for town {Name}."); + } + if(!currentMap.AccessRequirements.AreSatisfiedBy(requireables)) + { + return false; + } + if (currentMap.Right == null) + { + return false; + } + if(currentMap.RightExitIsOutside) + { + return true; + } + coveredMaps.Add(currentMap); + currentMap = currentMap.Right; + } + throw new ImpossibleException("Too many non-looping maps in town traversal check"); + } + + public void SetCollectables(IEnumerable collectables) + { + List collectableMaps = TownMaps.Where(i => i.Collectable != null).ToList(); + Debug.Assert(collectables.Count() == collectableMaps.Count); + int i = 0; + foreach(Collectable collectable in collectables) + { + collectableMaps[i++].Collectable = collectable; + } + } + + public bool ReplaceCollectable(Collectable toReplace, Collectable replacement) + { + TownMap? map = TownMaps.FirstOrDefault(i => i.Collectable == toReplace); + if(map == null) + { + return false; + } + map.Collectable = replacement; + return true; + } + + public TownMap? GetWizard() + { + VanillaTownMap map = Type switch + { + TownType.RAURU => VanillaTownMap.RAURU_WIZARD, + TownType.RUTO => VanillaTownMap.RUTO_WIZARD, + TownType.SARIA => VanillaTownMap.SARIA_WIZARD, + TownType.MIDO => VanillaTownMap.MIDO_WIZARD, + TownType.NABOORU => VanillaTownMap.NABOORU_WIZARD, + TownType.DARUNIA => VanillaTownMap.DARUNIA_WIZARD, + TownType.NEW_KASUTO => VanillaTownMap.NEW_KASUTO_WIZARD, + TownType.OLD_KASUTO => VanillaTownMap.OLD_KASUTO_WIZARD, + _ => throw new ImpossibleException("Unrecognized town type in GetWizard()") + }; + return GetTownMap(map); + } + + public TownMap? GetTownMap(int mapNumber) + { + return TownMaps.FirstOrDefault(i => i.Map == mapNumber); + } + + public TownMap? GetTownMap(VanillaTownMap map) + { + return GetTownMap((int)map); + } + + public bool HasShufflableItems() + { + return TownMaps.Any(i => i.CollectableIsShufflable); + } + + public string GenerateSpoiler() + { + return string.Join("\n", TownMaps.Where(i => i.Collectable != null).Select(i => $"\t{i.Name}: {i.Collectable?.EnglishText() ?? ""}")); + } +} diff --git a/RandomizerCore/Sidescroll/Town/TownMap.cs b/RandomizerCore/Sidescroll/Town/TownMap.cs new file mode 100644 index 000000000..322b2d93b --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/TownMap.cs @@ -0,0 +1,42 @@ +using Z2Randomizer.RandomizerCore.Enemy; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +public class TownMap +{ + public string Name { get; set; } = ""; + public int Map { get; set; } + public bool IsInternalLocation { get; } + public SideviewEditable Sideview { get; } + + public Collectable? Collectable { get; set; } + public bool CollectableIsShufflable { get; set; } + public TownMap? Left { get; set; } + public TownMap? Right { get; set; } + public TownMap? Door0 { get; set; } + public TownMap? Door1 { get; set; } + public TownMap? Door2 { get; set; } + public TownMap? Door3 { get; set; } + + //WTF should the script look like + + //For now, all screens that have collectables don't have separate requirements between access and collection. + //This may change in the future, which will require a bit of refactoring + public Requirements AccessRequirements { get; set; } = Requirements.NONE; + public EnemiesEditable Enemies { get; } + + //Some way to store what the Left/Right exit connection should be if it connects to the overworld. + //I'm fairly certain FC vs FD matters for saria, but I need to test it, if it doesn't matter it can just always be FC + //For now i'm assuming it doesn't matter and any null left/right connection is an external connection FC + //We can just test it when we have writing working and modify if needed + public bool LeftExitIsOutside { get; set; } + public bool RightExitIsOutside { get; set; } + + public TownMap(int mapNumber, byte[] sideviewsRaw, byte[] enemiesRaw, bool isInternalLocation) + { + Sideview = new(sideviewsRaw); + Enemies = new(enemiesRaw); + Map = mapNumber; + IsInternalLocation = isInternalLocation; + } +} \ No newline at end of file diff --git a/RandomizerCore/Sidescroll/Town/TownType.cs b/RandomizerCore/Sidescroll/Town/TownType.cs new file mode 100644 index 000000000..6233964c4 --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/TownType.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +public enum TownType +{ + INVALID = 0, + RAURU = 1, + RUTO = 2, + SARIA = 3, + MIDO = 4, + NABOORU = 5, + DARUNIA = 6, + NEW_KASUTO = 7, + OLD_KASUTO = 8, + BAGU = 9, +} + +public static class TownExtensions +{ + public const int SPELL_GET_START_ADDRESS = 0x17AF7; + public static int VanillaTownOrder(this TownType town) + { + return town switch + { + TownType.INVALID => 99, + TownType.RAURU => 1, + TownType.RUTO => 2, + TownType.SARIA => 3, + TownType.MIDO => 4, + TownType.NABOORU => 5, + TownType.DARUNIA => 6, + TownType.NEW_KASUTO => 7, + TownType.OLD_KASUTO => 8, + _ => 0 + }; + } + + public static string HintName(this TownType town) + { + return town switch + { + TownType.RAURU => "RAURU", + TownType.RUTO => "RUTO", + TownType.SARIA => "SARIA", + TownType.MIDO => "MIDO", + TownType.NABOORU => "NABOORU", + TownType.DARUNIA => "DARUNIA", + TownType.NEW_KASUTO => "NEW KASUTO", + TownType.OLD_KASUTO => "OLD KASUTO", + _ => "UNKNOWN" + }; + } +} + diff --git a/RandomizerCore/Sidescroll/Town/Towns.cs b/RandomizerCore/Sidescroll/Town/Towns.cs new file mode 100644 index 000000000..da17daaa6 --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/Towns.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +public class Towns +{ + + //There's almost certainly way cleaner way to do this but this works for now. + public static Town LoadVanillaTown(ROM rom, TownType townType, bool disableContainerRequirements) + { + Town town; + TownMap map; + switch(townType) + { + case TownType.RAURU: + town = rom.LoadTown(1, 2); + town.Name = "Rauru"; + map = town.GetTownMap(VanillaTownMap.RAURU_LEFT)!; + map.Name = "Rauru Left"; + map = town.GetTownMap(VanillaTownMap.RAURU_RIGHT)!; + map.Name = "Rauru Right"; + map = town.GetTownMap(VanillaTownMap.RAURU_WIZARD)!; + map.Name = "Rauru Wizard"; + map.Collectable = Collectable.SHIELD_SPELL; + map.AccessRequirements = disableContainerRequirements ? Requirements.NONE : new Requirements([RequirementType.ONE_CONTAINER]); + break; + case TownType.RUTO: + town = rom.LoadTown(4, 2); + town.Name = "Ruto"; + map = town.GetTownMap(VanillaTownMap.RUTO_LEFT)!; + map.Name = "Ruto Left"; + map = town.GetTownMap(VanillaTownMap.RUTO_RIGHT)!; + map.Name = "Ruto Right"; + map = town.GetTownMap(VanillaTownMap.RUTO_WIZARD)!; + map.Name = "Ruto Wizard"; + map.Collectable = Collectable.JUMP_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.TROPHY]) + : new Requirements([], [[RequirementType.TROPHY, RequirementType.TWO_CONTAINERS]]); + break; + case TownType.SARIA: + town = rom.LoadTown(6, 3); + town.Name = "Saria"; + map = town.GetTownMap(VanillaTownMap.SARIA_LEFT)!; + map.Name = "Saria Moat"; + map.AccessRequirements = new Requirements([RequirementType.FAIRY, RequirementType.BAGU_LETTER], [[RequirementType.JUMP, RequirementType.DASH]]); + map = town.GetTownMap(VanillaTownMap.SARIA_MID)!; + map.Name = "Saria Middle"; + map = town.GetTownMap(VanillaTownMap.SARIA_RIGHT)!; + map.Name = "Saria Right"; + map = town.GetTownMap(VanillaTownMap.SARIA_WIZARD)!; + map.Name = "Saria Wizard"; + map.Collectable = Collectable.LIFE_SPELL; + map.AccessRequirements = disableContainerRequirements ? + new Requirements([RequirementType.MIRROR]) : new Requirements([], [[RequirementType.MIRROR, RequirementType.THREE_CONTAINERS]]); + map = town.GetTownMap(VanillaTownMap.SARIA_TABLE)!; + map.Name = "Saria Table"; + map.Collectable = Collectable.MIRROR; + break; + case TownType.MIDO: + town = rom.LoadTown(9, 3); + town.Name = "Mido"; + map = town.GetTownMap(VanillaTownMap.MIDO_LEFT)!; + map.Name = "Mido Left"; + map = town.GetTownMap(VanillaTownMap.MIDO_MIDDLE)!; + map.Name = "Mido Middle"; + map = town.GetTownMap(VanillaTownMap.MIDO_RIGHT)!; + map.Name = "Mido Right"; + map = town.GetTownMap(VanillaTownMap.MIDO_WIZARD)!; + map.Name = "Mido Wizard"; + map.Collectable = Collectable.FAIRY_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.MEDICINE]) + : new Requirements([], [[RequirementType.MEDICINE, RequirementType.FOUR_CONTAINERS]]); + map = town.GetTownMap(VanillaTownMap.MIDO_TRAINER)!; + map.Name = "Downstab Guy"; + map.Collectable = Collectable.DOWNSTAB; + map.AccessRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); + break; + case TownType.NABOORU: + town = rom.LoadTown(12, 3); + town.Name = "Nabooru"; + map = town.GetTownMap(VanillaTownMap.NABOORU_LEFT)!; + map.Name = "Nabooru Left"; + map = town.GetTownMap(VanillaTownMap.NABOORU_MID)!; + map.Name = "Nabooru Fountain"; + map.Collectable = Collectable.WATER; + map = town.GetTownMap(VanillaTownMap.NABOORU_RIGHT)!; + map.Name = "Nabooru Right"; + map = town.GetTownMap(VanillaTownMap.NABOORU_WIZARD)!; + map.Name = "Nabooru Wizard"; + map.Collectable = Collectable.FIRE_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.WATER]) + : new Requirements([], [[RequirementType.WATER, RequirementType.FIVE_CONTAINERS]]); + break; + case TownType.DARUNIA: + town = rom.LoadTown(15, 3); + town.Name = "Darunia"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_LEFT)!; + map.Name = "Darunia Left"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_MID)!; + map.Name = "Darunia Middle"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_RIGHT)!; + map.Name = "Darunia Right"; + map = town.GetTownMap(VanillaTownMap.DARUNIA_WIZARD)!; + map.Name = "Darunia Wizard"; + map.Collectable = Collectable.REFLECT_SPELL; + map.AccessRequirements = disableContainerRequirements ? new Requirements([RequirementType.CHILD]) + : new Requirements([], [[RequirementType.CHILD, RequirementType.SIX_CONTAINERS]]); + map = town.GetTownMap(VanillaTownMap.DARUNIA_TRAINER)!; + map.Name = "Upstab Guy"; + map.Collectable = Collectable.UPSTAB; + map.AccessRequirements = new Requirements([RequirementType.JUMP, RequirementType.FAIRY]); + + break; + case TownType.NEW_KASUTO: + town = rom.LoadTown(18, 3); + town.Name = "New Kasuto"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_LEFT)!; + map.Name = "New Kasuto Left"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_MID)!; + map.Name = "New Kasuto Middle"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_RIGHT)!; + map.Name = "New Kasuto Right"; + map = town.GetTownMap(VanillaTownMap.NEW_KASUTO_WIZARD)!; + map.Name = "New Kasuto Wizard"; + map.Collectable = Collectable.SPELL_SPELL; + map.AccessRequirements = disableContainerRequirements ? Requirements.NONE : new Requirements([RequirementType.SEVEN_CONTAINERS]); + map = town.GetTownMap(VanillaTownMap.GRANNYS_BASEMENT)!; + map.Name = "Granny's Basement"; + map.AccessRequirements = new Requirements([RequirementType.SEVEN_CONTAINERS]); + map = town.GetTownMap(VanillaTownMap.SPELL_TOWER_INTERIOR)!; + map.Name = "Spell Tower" + + ""; + map.AccessRequirements = new Requirements([RequirementType.SPELL]); + break; + case TownType.OLD_KASUTO: + town = rom.LoadTown(21, 3); + town.Name = "Old Kasuto"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_LEFT)!; + map.Name = "Old Kasuto Left"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_MID)!; + map.Name = "Old Kasuto Left"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_RIGHT)!; + map.Name = "Old Kasuto Right"; + map = town.GetTownMap(VanillaTownMap.OLD_KASUTO_WIZARD)!; + map.Name = "Old Kasuto Wizard"; + map.Collectable = Collectable.THUNDER_SPELL; + map.AccessRequirements = disableContainerRequirements ? Requirements.NONE + : new Requirements([RequirementType.EIGHT_CONTAINERS]); + break; + case TownType.BAGU: + town = rom.LoadTown(24, 1); + town.Name = "Bagu's House"; + map = town.GetTownMap(VanillaTownMap.BAGU_EXTERNAL)!; + map.Name = "Bagu Exterior"; + map = town.GetTownMap(VanillaTownMap.BAGU_INDOORS)!; + map.Name = "Bagu Interior"; + map.Collectable = Collectable.BAGUS_NOTE; + break; + case TownType.INVALID: + throw new Exception($"Unrecognized vanilla town: {townType.HintName()}"); + default: + throw new Exception($"Unrecognized vanilla town: {townType.HintName()}"); + } + return town; + } +} diff --git a/RandomizerCore/Sidescroll/Town/VanillaTownMap.cs b/RandomizerCore/Sidescroll/Town/VanillaTownMap.cs new file mode 100644 index 000000000..7ba8985f1 --- /dev/null +++ b/RandomizerCore/Sidescroll/Town/VanillaTownMap.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Z2Randomizer.RandomizerCore.Sidescroll.Town; + +//Map indexes from the towns bank +public enum VanillaTownMap +{ + GLITCH_TOWN = 0, + RAURU_LEFT = 1, + RAURU_RIGHT = 2, + GLITCH_RUTO = 3, + RUTO_LEFT = 4, + RUTO_RIGHT = 5, + SARIA_LEFT = 6, + SARIA_MID = 7, + SARIA_RIGHT = 8, + MIDO_LEFT = 9, + MIDO_MIDDLE = 10, + MIDO_RIGHT = 11, + NABOORU_LEFT = 12, + NABOORU_MID = 13, + NABOORU_RIGHT = 14, + DARUNIA_LEFT = 15, + DARUNIA_MID = 16, + DARUNIA_RIGHT = 17, + NEW_KASUTO_LEFT = 18, + NEW_KASUTO_MID = 19, + NEW_KASUTO_RIGHT = 20, + OLD_KASUTO_LEFT = 21, + OLD_KASUTO_MID = 22, + OLD_KASUTO_RIGHT = 23, + BAGU_EXTERNAL = 24, + WIZARD_HOUSE_UPSTAIRS = 26, + SARIA_TABLE = 30, + BAGU_INDOORS = 31, + RAURU_WIZARD = 36, + RUTO_WIZARD = 37, + SARIA_WIZARD = 38, + MIDO_WIZARD = 39, + NABOORU_WIZARD = 40, + DARUNIA_WIZARD = 41, + NEW_KASUTO_WIZARD = 42, + OLD_KASUTO_WIZARD = 43, + MIDO_TRAINER = 44, + DARUNIA_TRAINER = 45, + GRANNYS_BASEMENT = 46, + SPELL_TOWER_INTERIOR = 47, +} diff --git a/RandomizerCore/Text.cs b/RandomizerCore/Text.cs index fb50e84b9..9353fe4fa 100644 --- a/RandomizerCore/Text.cs +++ b/RandomizerCore/Text.cs @@ -4,6 +4,7 @@ using System.Linq; using NLog; using Z2Randomizer.RandomizerCore.Overworld; +using Z2Randomizer.RandomizerCore.Sidescroll.Town; namespace Z2Randomizer.RandomizerCore; @@ -79,55 +80,36 @@ public bool ValidateDialogText() return true; } - public static Text GenerateHelpfulHint(List allLocations, Location location, Collectable collectable, bool useTownSpecificHints) + public static Text GenerateHelpfulHint(Location location, Collectable collectable, bool useTownSpecificHints) { string? hint = null; - if (location.PalaceNumber == 1) + if (location.Palace?.Number == 1) { hint = "horsehead$neighs$with the$%%"; } - else if (location.PalaceNumber == 2) + else if (location.Palace?.Number == 2) { hint = "helmethead$guards the$%%"; } - else if (location.PalaceNumber == 3) + else if (location.Palace?.Number == 3) { hint = "rebonack$rides$with the$%%"; } - else if (location.PalaceNumber == 4) + else if (location.Palace?.Number == 4) { hint = "carock$disappears$with the$%%"; } - else if (location.PalaceNumber == 5) + else if (location.Palace?.Number == 5) { hint = "gooma sits$on the$%%"; } - else if (location.PalaceNumber == 6) + else if (location.Palace?.Number == 6) { hint = "barba$slithers$with the$%%"; } - else if (useTownSpecificHints) + else if (useTownSpecificHints && location.Town != null) { - //For now bagu is not a town for hints. Could change in the future. - if (location.ActualTown != null && location.ActualTown != Town.BAGU) - { - hint = $"{((Town)location.ActualTown).HintName()}$has the$%%"; - } - //Saria table - if(allLocations.First(i => i.ActualTown == Town.SARIA_NORTH).Children.Contains(location)) - { - hint = $"{Town.SARIA_NORTH.HintName()}$has the$%%"; - } - //Nabooru fountain - if (allLocations.First(i => i.ActualTown == Town.NABOORU).Children.Contains(location)) - { - hint = $"{Town.NABOORU.HintName()}$has the$%%"; - } - //Spell Tower / Granny's Basement - if (allLocations.First(i => i.ActualTown == Town.NEW_KASUTO).Children.Contains(location)) - { - hint = $"{Town.NEW_KASUTO.HintName()}$has the$%%"; - } + hint = $"{((TownType)location.Town.Type!).HintName()}$has the$%%"; } if(hint == null) { diff --git a/RandomizerCore/Town.cs b/RandomizerCore/Town.cs deleted file mode 100644 index 2ba6c9621..000000000 --- a/RandomizerCore/Town.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; - -namespace Z2Randomizer.RandomizerCore; - -public enum Town -{ - INVALID = 0, - RAURU = 1, - RUTO = 2, - SARIA_NORTH = 3, - SARIA_SOUTH = 4, - MIDO_WEST = 5, - MIDO_CHURCH = 6, - NABOORU = 7, - DARUNIA_WEST = 8, - DARUNIA_ROOF = 9, - NEW_KASUTO = 10, - OLD_KASUTO = 11, - SARIA_TABLE = 12, - BAGU = 13, - NABOORU_FOUNTAIN = 14, -} - -public static class TownExtensions -{ - public const int SPELL_GET_START_ADDRESS = 0x17AF7; - public static int VanillaTownOrder(this Town town) - { - return town switch - { - Town.INVALID => 99, - Town.RAURU => 1, - Town.RUTO => 2, - Town.SARIA_NORTH => 3, - Town.MIDO_WEST => 4, - Town.NABOORU => 5, - Town.DARUNIA_WEST => 6, - Town.NEW_KASUTO => 7, - Town.OLD_KASUTO => 8, - _ => 0 - }; - } - - public static bool IsWizardTown(this Town town) - { - return town switch - { - Town.RAURU => true, - Town.RUTO => true, - Town.SARIA_NORTH => true, - Town.MIDO_WEST => true, - Town.NABOORU => true, - Town.DARUNIA_WEST => true, - Town.NEW_KASUTO => true, - Town.OLD_KASUTO => true, - Town.MIDO_CHURCH => true, - Town.DARUNIA_ROOF => true, - _ => false - }; - } - - public static string HintName(this Town town) - { - return town switch - { - Town.RAURU => "RAURU", - Town.RUTO => "RUTO", - Town.SARIA_NORTH => "SARIA", - Town.MIDO_WEST => "MIDO", - Town.NABOORU => "NABOORU", - Town.DARUNIA_WEST => "DARUNIA", - Town.NEW_KASUTO => "NEW KASUTO", - Town.OLD_KASUTO => "OLD KASUTO", - Town.MIDO_CHURCH => "MIDO", - Town.DARUNIA_ROOF => "DARUNIA", - _ => "UNKNOWN" - }; - } - - public static bool AppearsOnMap(this Town town) - { - return town switch - { - Town.RAURU => true, - Town.RUTO => true, - Town.SARIA_NORTH => true, - Town.SARIA_SOUTH => true, - Town.MIDO_WEST => true, - Town.MIDO_CHURCH => false, - Town.NABOORU => true, - Town.DARUNIA_WEST => true, - Town.DARUNIA_ROOF => false, - Town.NEW_KASUTO => true, - Town.OLD_KASUTO => true, - Town.SARIA_TABLE => false, - Town.BAGU => true, - Town.NABOORU_FOUNTAIN => false, - _ => throw new Exception("Unrecognized town: " + town.ToString()) - }; - } - - public static Town GetMasterTown(this Town town) - { - return town switch - { - Town.SARIA_TABLE => Town.SARIA_NORTH, - Town.DARUNIA_ROOF => Town.DARUNIA_WEST, - Town.NABOORU_FOUNTAIN => Town.NABOORU, - Town.MIDO_CHURCH => Town.MIDO_WEST, - _ => town - }; - } - - public static bool IsUnderConsiderationForReachable(this Town town) - { - return town switch - { - Town.RAURU => true, - Town.RUTO => true, - Town.SARIA_NORTH => true, - Town.SARIA_SOUTH => true, - Town.MIDO_WEST => true, - Town.MIDO_CHURCH => false, - Town.NABOORU => true, - Town.DARUNIA_WEST => true, - Town.DARUNIA_ROOF => false, - Town.NEW_KASUTO => true, - Town.OLD_KASUTO => true, - Town.SARIA_TABLE => false, - Town.BAGU => true, - Town.NABOORU_FOUNTAIN => false, - _ => throw new Exception("Unrecognized town: " + town.ToString()) - }; - } -} diff --git a/RandomizerCore/TownEnum.cs b/RandomizerCore/TownEnum.cs new file mode 100644 index 000000000..f4dcb6b47 --- /dev/null +++ b/RandomizerCore/TownEnum.cs @@ -0,0 +1,137 @@ +/* +using System; + +namespace Z2Randomizer.RandomizerCore; + +public enum TownEnum +{ + INVALID = 0, + RAURU = 1, + RUTO = 2, + SARIA_NORTH = 3, + SARIA_SOUTH = 4, + MIDO_WEST = 5, + MIDO_CHURCH = 6, + NABOORU = 7, + DARUNIA_WEST = 8, + DARUNIA_ROOF = 9, + NEW_KASUTO = 10, + OLD_KASUTO = 11, + SARIA_TABLE = 12, + BAGU = 13, + NABOORU_FOUNTAIN = 14, +} + +public static class TownExtensions +{ + public const int SPELL_GET_START_ADDRESS = 0x17AF7; + public static int VanillaTownOrder(this TownEnum town) + { + return town switch + { + TownEnum.INVALID => 99, + TownEnum.RAURU => 1, + TownEnum.RUTO => 2, + TownEnum.SARIA_NORTH => 3, + TownEnum.MIDO_WEST => 4, + TownEnum.NABOORU => 5, + TownEnum.DARUNIA_WEST => 6, + TownEnum.NEW_KASUTO => 7, + TownEnum.OLD_KASUTO => 8, + _ => 0 + }; + } + + public static bool IsWizardTown(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => true, + TownEnum.RUTO => true, + TownEnum.SARIA_NORTH => true, + TownEnum.MIDO_WEST => true, + TownEnum.NABOORU => true, + TownEnum.DARUNIA_WEST => true, + TownEnum.NEW_KASUTO => true, + TownEnum.OLD_KASUTO => true, + TownEnum.MIDO_CHURCH => true, + TownEnum.DARUNIA_ROOF => true, + _ => false + }; + } + + public static string HintName(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => "RAURU", + TownEnum.RUTO => "RUTO", + TownEnum.SARIA_NORTH => "SARIA", + TownEnum.MIDO_WEST => "MIDO", + TownEnum.NABOORU => "NABOORU", + TownEnum.DARUNIA_WEST => "DARUNIA", + TownEnum.NEW_KASUTO => "NEW KASUTO", + TownEnum.OLD_KASUTO => "OLD KASUTO", + TownEnum.MIDO_CHURCH => "MIDO", + TownEnum.DARUNIA_ROOF => "DARUNIA", + _ => "UNKNOWN" + }; + } + + public static bool AppearsOnMap(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => true, + TownEnum.RUTO => true, + TownEnum.SARIA_NORTH => true, + TownEnum.SARIA_SOUTH => true, + TownEnum.MIDO_WEST => true, + TownEnum.MIDO_CHURCH => false, + TownEnum.NABOORU => true, + TownEnum.DARUNIA_WEST => true, + TownEnum.DARUNIA_ROOF => false, + TownEnum.NEW_KASUTO => true, + TownEnum.OLD_KASUTO => true, + TownEnum.SARIA_TABLE => false, + TownEnum.BAGU => true, + TownEnum.NABOORU_FOUNTAIN => false, + _ => throw new Exception("Unrecognized town: " + town.ToString()) + }; + } + + public static TownEnum GetMasterTown(this TownEnum town) + { + return town switch + { + TownEnum.SARIA_TABLE => TownEnum.SARIA_NORTH, + TownEnum.DARUNIA_ROOF => TownEnum.DARUNIA_WEST, + TownEnum.NABOORU_FOUNTAIN => TownEnum.NABOORU, + TownEnum.MIDO_CHURCH => TownEnum.MIDO_WEST, + _ => town + }; + } + + public static bool IsUnderConsiderationForReachable(this TownEnum town) + { + return town switch + { + TownEnum.RAURU => true, + TownEnum.RUTO => true, + TownEnum.SARIA_NORTH => true, + TownEnum.SARIA_SOUTH => true, + TownEnum.MIDO_WEST => true, + TownEnum.MIDO_CHURCH => false, + TownEnum.NABOORU => true, + TownEnum.DARUNIA_WEST => true, + TownEnum.DARUNIA_ROOF => false, + TownEnum.NEW_KASUTO => true, + TownEnum.OLD_KASUTO => true, + TownEnum.SARIA_TABLE => false, + TownEnum.BAGU => true, + TownEnum.NABOORU_FOUNTAIN => false, + _ => throw new Exception("Unrecognized town: " + town.ToString()) + }; + } +} +*/ \ No newline at end of file diff --git a/RandomizerCore/Towns.cs b/RandomizerCore/Towns.cs deleted file mode 100644 index 88425b6f0..000000000 --- a/RandomizerCore/Towns.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; - -namespace Z2Randomizer.RandomizerCore; - -internal class Towns -{ - /* - public static readonly Dictionary townSpellAndItemRequirements = new() - { - { Town.RAURU, new Requirements() }, - { Town.RUTO, new Requirements([RequirementType.TROPHY]) }, - { Town.SARIA_NORTH, new Requirements([RequirementType.MIRROR]) }, - { Town.MIDO_WEST, new Requirements([RequirementType.MEDICINE]) }, - { Town.MIDO_CHURCH, new Requirements([RequirementType.JUMP, RequirementType.FAIRY]) }, - { Town.NABOORU, new Requirements([], [[RequirementType.WATER, RequirementType.FIVE_CONTAINERS]]) }, - { Town.DARUNIA_WEST, new Requirements([], [[RequirementType.CHILD, RequirementType.SIX_CONTAINERS]]) }, - { Town.DARUNIA_ROOF, new Requirements([RequirementType.JUMP, RequirementType.FAIRY]) }, - { Town.NEW_KASUTO, new Requirements([RequirementType.SEVEN_CONTAINERS]) }, - { Town.OLD_KASUTO, new Requirements([RequirementType.EIGHT_CONTAINERS]) }, - { Town.NABOORU_FOUNTAIN, new Requirements() }, - { Town.BAGU, new Requirements() }, - { Town.SARIA_TABLE, new Requirements() } - }; - */ - - public static readonly List STRICT_SPELL_LOCATIONS = [ Town.RAURU, Town.RUTO, Town.SARIA_NORTH, Town.MIDO_WEST, - Town.NABOORU, Town.DARUNIA_WEST, Town.NEW_KASUTO, Town.OLD_KASUTO ]; - - public static readonly List ITEM_LOCATION_TOWNS = [ Town.RAURU, Town.RUTO, Town.SARIA_NORTH, Town.MIDO_WEST, - Town.NABOORU, Town.DARUNIA_WEST, Town.NEW_KASUTO, Town.OLD_KASUTO, - Town.MIDO_CHURCH, Town.DARUNIA_ROOF, - Town.SARIA_TABLE, Town.NABOORU_FOUNTAIN, Town.BAGU]; -} diff --git a/RandomizerCore/Util.cs b/RandomizerCore/Util.cs index 7d97494be..c4564457d 100644 --- a/RandomizerCore/Util.cs +++ b/RandomizerCore/Util.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; using NLog; using Z2Randomizer.RandomizerCore.Overworld; -using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.RandomizerCore; @@ -98,17 +98,6 @@ public static string FromGameText(IEnumerable bytes) return output.ToString(); } - public static void Swap(Location p1, Location p2) - { - //(p2.Continent, p1.Continent) = (p1.Continent, p2.Continent); - (p2.Map, p1.Map) = (p1.Map, p2.Map); - (p2.PalaceNumber, p1.PalaceNumber) = (p1.PalaceNumber, p2.PalaceNumber); - - (p2.ActualTown, p1.ActualTown) = (p1.ActualTown, p2.ActualTown); - (p2.Collectables, p1.Collectables) = (p1.Collectables, p2.Collectables); - (p2.Name, p1.Name) = (p1.Name, p2.Name); - } - public static string ByteArrayToHexString(byte[] bytes) { string hex = BitConverter.ToString(bytes); diff --git a/Statistics/Result.cs b/Statistics/Result.cs index 9790d7dd9..793edde78 100644 --- a/Statistics/Result.cs +++ b/Statistics/Result.cs @@ -6,6 +6,7 @@ using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Overworld; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Statistics; diff --git a/Statistics/Statistics.cs b/Statistics/Statistics.cs index 11d2b74d9..ee4e5f4d1 100644 --- a/Statistics/Statistics.cs +++ b/Statistics/Statistics.cs @@ -6,6 +6,7 @@ using Desktop.Common; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Statistics; diff --git a/Tests/RoomSerializationTests.cs b/Tests/RoomSerializationTests.cs index 7a772db34..a154a39e0 100644 --- a/Tests/RoomSerializationTests.cs +++ b/Tests/RoomSerializationTests.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Tests; diff --git a/Tests/Utils.cs b/Tests/Utils.cs index ff526a1cc..6a9f322c0 100644 --- a/Tests/Utils.cs +++ b/Tests/Utils.cs @@ -5,6 +5,7 @@ using System.Text.RegularExpressions; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; namespace Z2Randomizer.Tests; diff --git a/ValidateRooms/Program.cs b/ValidateRooms/Program.cs index a005b7101..7273bff1d 100644 --- a/ValidateRooms/Program.cs +++ b/ValidateRooms/Program.cs @@ -6,6 +6,7 @@ using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Enemy; using Z2Randomizer.RandomizerCore.Sidescroll; +using Z2Randomizer.RandomizerCore.Sidescroll.Palace; using static Z2Randomizer.RandomizerCore.Util; StringBuilder sb = new StringBuilder(""); From 230488b5ea826462e93a5d1f51c7ad7105d19206 Mon Sep 17 00:00:00 2001 From: Patrick McNeill Date: Sun, 31 May 2026 20:03:02 -0400 Subject: [PATCH 083/128] Flag/seed pasting convenience, filename formatting --- .../ViewModels/GenerateRomViewModel.cs | 9 +- .../ViewModels/RandomizerViewModel.cs | 28 +++--- .../ViewModels/Tabs/SpritePreviewViewModel.cs | 6 ++ CrossPlatformUI/Views/RandomizerView.axaml | 2 +- CrossPlatformUI/Views/RandomizerView.axaml.cs | 24 ++++++ .../Views/Tabs/SpritePreviewView.axaml | 15 ++++ RandomizerCore/FlagPasteParser.cs | 85 +++++++++++++++++++ RandomizerCore/OutputFilenameFormatter.cs | 58 +++++++++++++ RandomizerCore/RandomizerConfiguration.cs | 4 + Tests/FlagsTests.cs | 85 +++++++++++++++++++ 10 files changed, 302 insertions(+), 14 deletions(-) create mode 100644 RandomizerCore/FlagPasteParser.cs create mode 100644 RandomizerCore/OutputFilenameFormatter.cs diff --git a/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs b/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs index 5a0075651..b6d43f1ee 100644 --- a/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs +++ b/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs @@ -4,6 +4,7 @@ using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Subjects; +using System.IO; using System.Reflection; using System.Text.Json.Serialization; using System.Threading; @@ -97,8 +98,12 @@ async void GenerateSeed() if(!tokenSource.IsCancellationRequested && output.success) { var flags = config.SerializeFlags(); - var basename = $"Z2_{config.Seed}_{flags}"; - var filename = basename + ".nes"; + var filename = OutputFilenameFormatter.Format(config.OutputFilenameTemplate, flags, config.Seed, randomizer.Hash); + var basename = Path.GetFileNameWithoutExtension(filename); + if (string.IsNullOrEmpty(basename)) + { + basename = filename; + } await files.SaveGeneratedBinaryFile(filename, output.romdata!, Main.OutputFilePath); #if DEBUG var debugfile = basename + ".mlb"; diff --git a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs index 8e45f03fa..17c42d728 100644 --- a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs +++ b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs @@ -28,22 +28,28 @@ public class RandomizerViewModel : ReactiveValidationObject, IRoutableViewModel, [JsonIgnore] public BehaviorSubject FlagsValidSubject = new(true); - private bool IsFlagStringValid(string flags) + private static bool IsFlagStringValid(string flags) => FlagPasteParser.IsValidFlagString(flags); + + private string flagInput = ""; + + [JsonIgnore] + public string FlagInput { - try - { - _ = new RandomizerConfiguration(flags); - return true; - } - catch + get => flagInput; + set { - return false; + var trimmedValue = value?.Trim() ?? ""; + var (extractedFlags, extractedSeed) = FlagPasteParser.Parse(trimmedValue); + + if (Main is not null && !string.IsNullOrEmpty(extractedSeed)) + { + Main.Config.Seed = extractedSeed; + } + + this.RaiseAndSetIfChanged(ref flagInput, extractedFlags ?? trimmedValue); } } - [JsonIgnore] - public string FlagInput { get; set { field = value.Trim(); this.RaisePropertyChanged(); } } = ""; - [JsonIgnore] public string Seed { diff --git a/CrossPlatformUI/ViewModels/Tabs/SpritePreviewViewModel.cs b/CrossPlatformUI/ViewModels/Tabs/SpritePreviewViewModel.cs index 782fe83bf..ab7bdf281 100644 --- a/CrossPlatformUI/ViewModels/Tabs/SpritePreviewViewModel.cs +++ b/CrossPlatformUI/ViewModels/Tabs/SpritePreviewViewModel.cs @@ -63,6 +63,12 @@ public BeamSprites BeamSprite set { Main.Config.BeamSprite = value; this.RaisePropertyChanged(); } } + public string OutputFilenameTemplate + { + get => Main.Config.OutputFilenameTemplate; + set { Main.Config.OutputFilenameTemplate = value; this.RaisePropertyChanged(); } + } + public byte spriteTunicColor { get; private set; } public byte spriteSkinTone { get; private set; } public byte spriteOutlineColor { get; private set; } diff --git a/CrossPlatformUI/Views/RandomizerView.axaml b/CrossPlatformUI/Views/RandomizerView.axaml index 9918c2020..43dc2d822 100644 --- a/CrossPlatformUI/Views/RandomizerView.axaml +++ b/CrossPlatformUI/Views/RandomizerView.axaml @@ -19,7 +19,7 @@ - + diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index df3e1951f..5db536c97 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -278,13 +278,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC reachableAreas = new HashSet(); //areasByLocation = new SortedDictionary>(); - byte[] correctVanillaHash = [0x76, 0x4D, 0x36, 0xFA, 0x8A, 0x24, 0x50, 0x83, 0x4D, 0xA5, 0xE8, 0x19, 0x42, 0x81, 0x03, 0x5A]; - var vanillaRomHash = MD5Hash.ComputeHash(vanillaRomData); - if (!correctVanillaHash.SequenceEqual(vanillaRomHash)) - { - throw new UserFacingException("Vanilla ROM checksum failure", "Please provide an unmodified Zelda 2 ROM (US release)."); - } - + ROM.ValidateVanillaRom(ref vanillaRomData); // Make a copy of the vanilla data to prevent seed bleed ROMData = new ROM(vanillaRomData.ToArray(), true); diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index fd3becaff..835c0864f 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -2788,4 +2788,36 @@ public void UpdateItem(Collectable item, Room room) logger.Warn($"Could not write Collectable {item} to Item room {room.GetDebuggerDisplay()} in palace {room.PalaceNumber}"); //throw new Exception("Could not write Collectable to Item room in palace " + PalaceNumber); } + + /// Validates the ROM. Tries to fix headerless or bad header ROMs. + public static void ValidateVanillaRom(ref byte[] vanillaRomData) + { + int correctVanillaLengthWithHeader = 1024 * 256 + 0x10; + byte[] iNesHeader = [0x4e, 0x45, 0x53, 0x1a, 0x08, 0x10, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; // (gets overriden with ROM expansion) + byte[] correctVanillaHash = [0x76, 0x4D, 0x36, 0xFA, 0x8A, 0x24, 0x50, 0x83, 0x4D, 0xA5, 0xE8, 0x19, 0x42, 0x81, 0x03, 0x5A]; + + if (vanillaRomData.Length != correctVanillaLengthWithHeader) + { + if (vanillaRomData.Length == correctVanillaLengthWithHeader - 0x10) + { + vanillaRomData = [.. iNesHeader, .. vanillaRomData]; + } + else + { + throw new UserFacingException("Invalid ROM size", "Please provide an unmodified Zelda 2 ROM (US release) with or without header."); + } + } + else + { + for (int i = 0; i < 0x10; i++) + { + vanillaRomData[i] = iNesHeader[i]; + } + } + var vanillaRomHash = MD5Hash.ComputeHash(vanillaRomData); + if (!correctVanillaHash.SequenceEqual(vanillaRomHash)) + { + throw new UserFacingException("Vanilla ROM checksum failure", "Please provide an unmodified Zelda 2 ROM (US release)."); + } + } } From 533c8fd50483691d74dde35f6f3ba57f93b36e13 Mon Sep 17 00:00:00 2001 From: initsu Date: Fri, 12 Jun 2026 22:53:28 +0200 Subject: [PATCH 092/128] Ask for a new ROM output folder if ROM writing throws --- CrossPlatformUI/Services/FileDialogService.cs | 2 +- .../ViewModels/GenerateRomViewModel.cs | 25 ++++++++++++++++++- .../ViewModels/RandomizerViewModel.cs | 17 ++++++++++--- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/CrossPlatformUI/Services/FileDialogService.cs b/CrossPlatformUI/Services/FileDialogService.cs index 062173118..a0704866f 100644 --- a/CrossPlatformUI/Services/FileDialogService.cs +++ b/CrossPlatformUI/Services/FileDialogService.cs @@ -21,7 +21,7 @@ public class FileDialogService(TopLevel? target) : IFileDialogService { var files = await target!.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions() { - Title = "Open Folder", + Title = "Select ROM Output Folder", AllowMultiple = false }); diff --git a/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs b/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs index b5b4fe56b..6106c28f7 100644 --- a/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs +++ b/CrossPlatformUI/ViewModels/GenerateRomViewModel.cs @@ -105,7 +105,30 @@ async void GenerateSeed() { basename = filename; } - await files.SaveGeneratedBinaryFile(filename, output.romdata!, Main.OutputFilePath); + bool saved = false; + while (!saved) + { + try + { + await files.SaveGeneratedBinaryFile(filename, output.romdata!, Main.OutputFilePath); + saved = true; + } + catch (Exception e) + { + // DirectoryNotFoundException, UnauthorizedAccessException + // We could check for specific exceptions, but it's + // probably fine to do this for all errors while saving + var newFolder = await RandomizerViewModel.SelectSaveFolder(); + if (!string.IsNullOrEmpty(newFolder) && newFolder != Main.OutputFilePath) + { + Main.OutputFilePath = newFolder; + } + else // user did not pick a new save folder + { + throw new UserFacingException("Unable to save ROM to folder", e.Message); + } + } + } #if DEBUG var debugfile = basename + ".mlb"; if (!string.IsNullOrEmpty(output.debuginfo)) diff --git a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs index 17c42d728..8cf82394a 100644 --- a/CrossPlatformUI/ViewModels/RandomizerViewModel.cs +++ b/CrossPlatformUI/ViewModels/RandomizerViewModel.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Text.Json.Serialization; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Avalonia.Controls; using ReactiveUI; @@ -117,9 +118,7 @@ public RandomizerViewModel(MainViewModel main) SaveFolder = ReactiveCommand.CreateFromTask(async () => { - var fileDialog = App.Current?.Services?.GetService()!; - var folder = await fileDialog.OpenFolderAsync(); - Main.OutputFilePath = folder?.Path.LocalPath ?? ""; + Main.OutputFilePath = await SelectSaveFolder() ?? ""; }); CheckForUpdates = ReactiveCommand.CreateFromTask(async () => @@ -203,6 +202,18 @@ public RandomizerViewModel(MainViewModel main) this.WhenActivated(OnActivate); } + public static async Task SelectSaveFolder() + { + var fileDialog = App.Current?.Services?.GetService()!; + var folder = await fileDialog.OpenFolderAsync(); + Uri? path = folder?.Path; + // both LocalPath and TryGetLocalPath() throw for non-absoloute URIs + string? localPath = path?.IsAbsoluteUri == true + ? path.LocalPath + : path?.OriginalString; + return localPath; + } + private void OnActivate(CompositeDisposable disposables) { var loadedFlags = Main.Config.SerializeFlags(); // this serializes the configuration From 09f34216d48ce7503675d1ec2fcc02e31cc5f5bb Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 11 Jul 2026 13:20:05 -0400 Subject: [PATCH 093/128] WIP Flute still mostly working this time for real --- CommandLine/Properties/launchSettings.json | 2 +- Desktop.Common/DesktopJsEngine.cs | 2 +- Directory.Build.props | 1 - Directory.Packages.props | 6 +- RandomizerCore/Asm/Flute.s | 198 +++++++++++++++------ RandomizerCore/Asm/StatTracking.s | 1 - RandomizerCore/Asm/ram.inc | 5 +- RandomizerCore/Asm/z2mario/map_mario.chr | Bin 2048 -> 1024 bytes RandomizerCore/Asm/z2mario/mario.s | 10 +- RandomizerCore/Asm/z2r.inc | 1 + RandomizerCore/Hyrule.cs | 46 ++--- Z2Randomizer.sln | 1 + 12 files changed, 184 insertions(+), 89 deletions(-) diff --git a/CommandLine/Properties/launchSettings.json b/CommandLine/Properties/launchSettings.json index d85a269e3..5f265d989 100644 --- a/CommandLine/Properties/launchSettings.json +++ b/CommandLine/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "CommandLine": { "commandName": "Project", - "commandLineArgs": "-f AAABvvyhAmLAAAAAAAAAEuPi+yAAAAAAAAAAAAAWAA -r ../Zelda2.nes -s 123456789 -o ..", + "commandLineArgs": "-f AAABvvyhAmLAAAAAAAAAEuPi+yAAAAAAAAAAAAAbhA -r ../Zelda2.nes -s mario -o ..", "workingDirectory": "../build/" } } diff --git a/Desktop.Common/DesktopJsEngine.cs b/Desktop.Common/DesktopJsEngine.cs index 1895eb1ae..3e0c064f1 100644 --- a/Desktop.Common/DesktopJsEngine.cs +++ b/Desktop.Common/DesktopJsEngine.cs @@ -4,7 +4,7 @@ namespace Desktop.Common; [UnsupportedOSPlatform("browser")] -public class DesktopJsEngine(Js65Options? options = null, bool debugJavascript = true) +public class DesktopJsEngine(Js65Options? options = null, bool debugJavascript = false) #pragma warning disable CA1416 : ClearScriptEngine(options, true, debugJavascript); #pragma warning restore CA1416 diff --git a/Directory.Build.props b/Directory.Build.props index bd965de9d..5f2c87277 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,6 @@ net10.0 enable - 1.0.7 11.3.12 0.10.4 3.13.4 diff --git a/Directory.Packages.props b/Directory.Packages.props index c10f97ee2..7c8797232 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ true 5.3.0 7.5.1 - 1.0.7 + 1.0.9 12.0.4 3.119.4 @@ -14,6 +14,8 @@ + + @@ -71,5 +73,7 @@ + + \ No newline at end of file diff --git a/RandomizerCore/Asm/Flute.s b/RandomizerCore/Asm/Flute.s index e7cb4b078..80401c4a2 100644 --- a/RandomizerCore/Asm/Flute.s +++ b/RandomizerCore/Asm/Flute.s @@ -1,21 +1,16 @@ .include "z2r.inc" -; Flute Twister Warp -; When Link plays the flute on the overworld and neither a river-devil tile -; (OverworldFacingTerrain==$0F) nor the three-eye-rock tile is adjacent, a twister -; spawns at the left screen edge and travels rightward. Touching it -; warps Link into the next or previous gem-placed dungeon in sequence. -; Face right/up = next palace; face left/down = previous palace. -; The anchor is the last dungeon Link entered or warped to (TwisterCurrentPalaceSlot). - -TWISTER_TILE = $E0 ; tile ID for frame 0; frame 1 uses TWISTER_TILE+1 +TWISTER_TILE = $b5 ; tile IDs for frame 0; frame 1 uses TWISTER_TILE+2 TWISTER_PAL = $02 ; OAM palette attribute (bits 0-1) .segment "PRG0", "PRG7" +.import PalaceMappingTable + ProcessNextOverworldEnemy = $842F UpdateOverworldSpritePosition = $8397 AreaLoadTrigger = $85eb +OverworldPostLoad = $C72F ; Patches the place where the enemy graphics data is loaded ; so we can draw a custom tornado @@ -34,9 +29,36 @@ AreaLoadTrigger = $85eb .org $85A9 jsr RecordPalaceEntry +; When taking a cross continent flute, we load the overworld and then the dungeon. +; Hook right before we reveal the overworld and teleport to the dungeon +.org OverworldPostLoad + jsr OverworldPostLoadFluteCheck + +.segment "PRG7" + +.reloc +OverworldPostLoadFluteCheck: + lda TwisterPendingPalaceLocation + bne @enter + ; just a regular overworld load + lda #$00 + sta $0726 + rts +@enter: + sta LocationNumber ; restore the palace area index ($34 + palace code) + lda #$00 + sta TwisterPendingPalaceLocation + ; skip the main overworld game mode and move to the side view loading + lda #5 + sta GameMode + rts + + +.segment "PRG0", "PRG7" + .reloc TwisterDrawCheck: - lda OverworldEnemy0Type, x ; reproduce overwritten LDA $82,x + lda OverworldEnemy0Type, x cmp #$04 beq @twister tax ; do the original @@ -44,11 +66,14 @@ TwisterDrawCheck: @twister: ; type 4: write twister tile and palette ; Y = slot*16 (OAM base), set before this hook - lda R7 ; animation frame (0 or 1) - clc + lda FrameCounter + and #$10 + lsr + lsr adc #TWISTER_TILE - sta $0281, y ; left sprite tile - sta $0285, y ; right sprite tile + sta $0281, y ; left sprite tile + adc #2 ; carry is clear + sta $0285, y ; right sprite tile lda #TWISTER_PAL sta $0282, y sta $0286, y @@ -87,74 +112,127 @@ TwisterHitCheck: .reloc TwisterWarp: - ; Direction: OverworldFacingDirection bits: $01=right, $08=up = NEXT; $02=left, $04=down = PREV + ; Direction: OverworldFacingDirection bits + ; next $01=right $08=up + ; previous $02=left $04=down lda OverworldFacingDirection ; right ($01) or up ($08) and #$09 bne @go_next - lda #$05 ; add 5 to wrap around and go backwards in the list + lda #$0f ; add $0f (== -1 mod 16) to walk the slot list backwards bne @scan ; unconditional @go_next: - lda #$01 ; add 1 to go forward in the list + lda #$01 ; add 1 to walk the slot list forwards @scan: sta R7 - ; a = current palace slot we want to check + ; scan each of the possible palace global spots in the PalaceMappingTable lda TwisterCurrentPalaceSlot - ; x = 6 is the loop counter for the number of palaces to check - ldx #$06 + ldx #$10 @next_palace: - ; increment or decrement the list but clamp the result between 0 and 5 clc adc R7 - bmi @below_zero - cmp #$06 - bcc @no_wrap - sec - sbc #$06 - jmp @no_wrap -@below_zero: - clc - adc #$06 -@no_wrap: - pha ; save candidate palace_code + ; wrap within the 0-15 global slot space + and #$0f + tay + lda PalaceMappingTable, y + cmp #$ff ; $ff is a place holder indicating no palace at this slot in the world + beq @advance + ; Palace exists here. Check whether its gem has been placed. + sty R6 + ; This table layout was optimized for updating stats (since thats done every frame) + ; and this doesn't matter how long it takes pretty much + sec + sbc #Palace1Offset + ; div by 3 to get the palace number 0 - 6 + sta R5 + lsr + lsr + adc R5 + ror + lsr + adc R5 + ror + lsr + adc R5 + ror + lsr + adc R5 + ror + lsr + ; Check for great palace ID and skip it (palace 6) + cmp #6 + bcs @advance + ; A = CrystalPlaced index 0-5 tay - lda CrystalPlaced, y ; gem placed at this palace? - bne @found - pla + lda CrystalPlaced, y + bne @crystalPlacedAtPalace + ; Check next palace slot in the real locations table + ldy R6 +@advance: + tya dex bne @next_palace - rts ; there should always be a valid palace?? the twister shouldn't spawn otherwise -@found: - pla ; A = target palace_code + rts ; this shouldn't happen. tornado should only spawn if a gem is placed +@crystalPlacedAtPalace: + ; R6 = target global slot + lda R6 sta TwisterCurrentPalaceSlot ; clear the twister slot - ldx OverworldCurrentSlot ; slot index saved at $80 by LDX $80 / STX $80 in L8332 + ldx OverworldCurrentSlot lda #$00 sta OverworldEnemy0Type, x sta OverworldDemonTimer, x - ; set LocationNumber to palace area index ($34 + palace_code) - lda TwisterCurrentPalaceSlot + ; LocationNumber = $34 + within-region palace code + lda R6 + and #$03 clc adc #$34 sta LocationNumber - + ; target region = global slot >> 2 + lda R6 + lsr + lsr + cmp RegionNumber + ; already in the right continent so we don't need to switch continents + beq @same_region + ; Different continent so we need to reload the continent + ldx RegionNumber + stx PreviousRegion + sta RegionNumber + ; the new game mode will load the WorldNumber + lda #$00 + sta WorldNumber + lda #$01 + sta GoingOutsideFlag + lda LocationNumber + sta TwisterPendingPalaceLocation + ; AreaLoadTrigger does `inc GameMode` and we want mode 0 so set it to $ff + lda #$ff + sta GameMode +@same_region: pla pla jmp AreaLoadTrigger .reloc RecordPalaceEntry: - ; reproduce overwritten STX $0748 stx LocationNumber - ; update anchor when entering a palace (area index $34-$39 = palace codes 0-5) + ; update anchor when Link enters a palace. Palace area indices are $34-$36 + ; (within-region palace codes 0-2). Combine with the region for a global slot. txa - ; negative: not a palace sec sbc #$34 - bcc @done - ; >= 6: not a valid palace code - cmp #$06 - bcs @done + bcc @done ; below $34: not a palace + cmp #$03 + bcs @done ; $37+: not a palace entrance + ; A = within-region palace code (0-2); global slot = region*4 + code + pha + lda RegionNumber + asl + asl + sta TwisterCurrentPalaceSlot + pla + ora TwisterCurrentPalaceSlot sta TwisterCurrentPalaceSlot @done: rts @@ -193,16 +271,16 @@ free_slot: init_slot: lda #$04 sta OverworldEnemy0Type, x ; type 4 = twister - lda #$01 - sta OverworldDemonTimer, x ; any non-zero value keeps slot active + lda #$40 + sta OverworldDemonTimer, x ; lda OverworldScrollY ; camera Y clc - adc #$70 ; center-screen height + adc #$6c ; center-screen height sta OverworldEnemyYPosition, x ; world Y lda ScrollPosShadow ; camera X sta OverworldEnemyXPosition, x ; world X = left screen edge (OAM X = 0) - lda #$01 - sta OverworldDemonXVelocity, x ; +1/frame rightward + lda #$02 + sta OverworldDemonXVelocity, x ; +2/frame rightward lda #$00 sta OverworldDemonYVelocity, x ; no vertical movement ; Twister spawned. Return from flute dispatch (don't fall into L838F). @@ -216,8 +294,11 @@ spider: .segment "PRG1" -; Hook: Replace BEQ L838F; RTS (3 bytes at $838C) with JSR TwisterSpawnPRG1. -; JSR pushes PC+2 = $838E; RTS inside TwisterSpawnPRG1 returns to $838F = L838F. +; Skip east continent check +.org $8368 + jmp $836f +FREE_UNTIL $836f + .org $838C jsr TwisterSpawnPRG1 ; The code after the patch point is where it normally branches to if the flute @@ -232,6 +313,11 @@ spider: .segment "PRG2" +; Skip east continent check +.org $8368 + jmp $836f +FREE_UNTIL $836f + ; Same hook at the same address. .org $838C jsr TwisterSpawnPRG2 diff --git a/RandomizerCore/Asm/StatTracking.s b/RandomizerCore/Asm/StatTracking.s index d049fcf0e..e75a2e7a5 100644 --- a/RandomizerCore/Asm/StatTracking.s +++ b/RandomizerCore/Asm/StatTracking.s @@ -117,7 +117,6 @@ StatTrackDownStab: ; to the "real palace" at this offset. This way if Palace 5 is in the location of Palace 1, ; then we increment the correct timer for Palace 5 .reloc -Palace1Offset = StatTimeInPalace1 - StatTimeAtLocation .export PalaceMappingTable PalaceMappingTable: ; region 0 - east hyrule diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index 4bd143344..569a4e2c3 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -369,6 +369,8 @@ EntryFacing = $0701 ; Sideview entry facing direction. 0=Fa EnterFromElevatorAbove = $0704 ; >0 Link enters the room from the top EnterCentered = $0705 ; >0 Link enters centered; like at North Palace, taking an elevator exit or entering a random encounter RegionNumber = $0706 ; 0=West, 1=Death Mountain, 2=East, 3=Maze Island +GoingOutsideFlag = $0709 ; non-zero routes game mode 0 (load area data) out to the overworld via go_outside +PreviousRegion = $070a ; RegionNumber Link came from WorldNumber = $0707 ; 0=Caves & enemy encounters, 1=West towns, 2=East towns, 3=Palace 1/2/5, 4=Palace 3/4/6, 5=Great Palace ScrollDirection = $071f ; Current scroll direction. 1=Scrolling right, 2=Scrolling left ColumnScrollDirection = $0720 ; Scroll direction that is updated only when scrolling crosses a column boundary @@ -515,7 +517,8 @@ RESV BlockBreakColumnOffset, BREAKABLE_BLOCK_COUNT ; Vanilla addr $042E - Offset RESV BlockBreakAddrLo, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table lo addr RESV BlockBreakAddrHi, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table hi addr -RESV TwisterCurrentPalaceSlot ; palace_code (0-5) of last dungeon Link entered or warped to +RESV TwisterCurrentPalaceSlot ; global palace slot (RegionNumber*4 + within-region palace code) of last dungeon Link entered or warped to +RESV TwisterPendingPalaceLocation ; flute cross-continent warp: non-zero = palace LocationNumber to enter once the destination region's overworld has been reloaded .ifdef ENABLE_Z2_MARIO diff --git a/RandomizerCore/Asm/z2mario/map_mario.chr b/RandomizerCore/Asm/z2mario/map_mario.chr index a3f7db9bf6eb3692d11c7bda6bce302231da23e5..ebd3f2454ca18dc8ab2b17b6d5e9748d9b4382b5 100644 GIT binary patch delta 140 zcmWm6u?@o@5CA|x$fBU2bO#&6Wr|3TH(>;qsr;nR4qhQr7GM;QKxMrm!pAADz3j4i zMTvmSj2sy|dwB{?&h0V3XF01MgRwoO delta 26 dcmZqRXb{-oz_|G!qdn8+3ycO3V)6w>MF4mu2(bVF diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index c7db73cae..6886b0654 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -375,10 +375,12 @@ PlayerSubs: CntPl: lda GameEngineSubroutine ;if executing specific game engine routine, cmp #$0b ;branch ahead to some other part - jeq PlayerKilled - lda PlayerChangeSizeFlag ;if grow/shrink flag set - jne DoChangeSize ;then branch to some other code - + bne + + jmp PlayerKilled ++ lda PlayerChangeSizeFlag ;if grow/shrink flag set + beq + + jmp DoChangeSize ;then branch to some other code ++ jsr FindPlayerAction ;otherwise jump and return lda SwimmingFlag diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 8368b2c65..d82b26c9f 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -217,6 +217,7 @@ RESV StatTimeInPalace4, 3 RESV StatTimeInPalace5, 3 RESV StatTimeInPalace6, 3 RESV StatTimeInPalaceGP, 3 +Palace1Offset = StatTimeInPalace1 - StatTimeAtLocation ; All of these timestamps are checkpointed such that if you game over, ; we restore from the most recent version of the checkpoint. So for any stats diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 5db536c97..d866ff5d2 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -579,7 +579,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC private Assembler CreateAssemblyEngine() { - var asm = NewAssembler(assemblerOptions, true); + var asm = NewAssembler(assemblerOptions, false); asm.Callbacks = new Js65Callbacks { OnFileReadText = AsmFileReadTextCallback, @@ -1380,28 +1380,28 @@ private async Task FillPalaceRooms(AsmModule sideviewModule) sideviewModule.Byt(itemBits); } - try - { - ROM testRom = new(ROMData); - Random testRng = new(SeedHash); - //This continues to get worse, the text is based on the palaces and asm patched, so it needs to - //be tested here, but we don't actually know what they will be until later, for now i'm just - //testing with the vanilla text, but this could be an issue down the line. - ApplyAsmPatches(props, validationEngine, testRng, ROMData.GetGameText(), testRom, new StatRandomizer(testRom, props)); - validationEngine.Add(sideviewModule); - await testRom.ApplyAsm(validationEngine); //.Wait(ct); - } - catch (Exception e) - { - // Microsoft.ClearScript.ScriptEngine needs to be abstracted for browser - if (e.Message.Contains("Could not find space for")) - { - logger.Debug(e, "Room packing failed. Retrying."); - return false; - } - logger.Error(e, "Failed to build assembly patches"); - throw; - } + // try + // { + // ROM testRom = new(ROMData); + // Random testRng = new(SeedHash); + // //This continues to get worse, the text is based on the palaces and asm patched, so it needs to + // //be tested here, but we don't actually know what they will be until later, for now i'm just + // //testing with the vanilla text, but this could be an issue down the line. + // ApplyAsmPatches(props, validationEngine, testRng, ROMData.GetGameText(), testRom, new StatRandomizer(testRom, props)); + // validationEngine.Add(sideviewModule); + // await testRom.ApplyAsm(validationEngine); //.Wait(ct); + // } + // catch (Exception e) + // { + // // Microsoft.ClearScript.ScriptEngine needs to be abstracted for browser + // if (e.Message.Contains("Could not find space for")) + // { + // logger.Debug(e, "Room packing failed. Retrying."); + // return false; + // } + // logger.Error(e, "Failed to build assembly patches"); + // throw; + // } return true; } diff --git a/Z2Randomizer.sln b/Z2Randomizer.sln index 22545c485..21e7ab8d4 100644 --- a/Z2Randomizer.sln +++ b/Z2Randomizer.sln @@ -22,6 +22,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets GlobalUsings.cs = GlobalUsings.cs + Directory.Packages.props = Directory.Packages.props EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DumpRooms", "DumpRooms\DumpRooms.csproj", "{25230EEF-2BB9-4472-96E3-40AE6EDCF5DE}" From b34ecf74f4406403a37d40edb64495686cb14b65 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sun, 12 Jul 2026 23:47:51 -0400 Subject: [PATCH 094/128] Fix outstanding issues with flute -> palace warp --- RandomizerCore/Asm/Flute.s | 250 +++++++++++++---------- RandomizerCore/Asm/ram.inc | 58 +++--- RandomizerCore/Asm/z2mario/integration.s | 10 + RandomizerCore/Asm/z2mario/sfx.s | 19 +- RandomizerCore/Hyrule.cs | 6 +- 5 files changed, 200 insertions(+), 143 deletions(-) diff --git a/RandomizerCore/Asm/Flute.s b/RandomizerCore/Asm/Flute.s index 80401c4a2..1aac6e855 100644 --- a/RandomizerCore/Asm/Flute.s +++ b/RandomizerCore/Asm/Flute.s @@ -1,8 +1,17 @@ .include "z2r.inc" +TWISTER_ENEMY_ID = $04 TWISTER_TILE = $b5 ; tile IDs for frame 0; frame 1 uses TWISTER_TILE+2 TWISTER_PAL = $02 ; OAM palette attribute (bits 0-1) +; Overworld "Area Byte 0" (Y position + flags) table in work RAM, indexed by +; LocationNumber. Bit 7 set marks the slot as a palace/town entrance; when it's +; clear the sideview loader treats the slot as a generic/random-battle area. +OverworldAreaTable = $6a00 +; UpdateHiddenPalaceSpot() stores the flute-hidden palace's *revealed* Area Data +; The palace is index 0 at $df68 and the town is index 1 at $df69 +HiddenPalaceRevealedAreaUpdate = $df68 + .segment "PRG0", "PRG7" .import PalaceMappingTable @@ -46,11 +55,21 @@ OverworldPostLoadFluteCheck: rts @enter: sta LocationNumber ; restore the palace area index ($34 + palace code) + sta AreaEntranceIndex lda #$00 sta TwisterPendingPalaceLocation ; skip the main overworld game mode and move to the side view loading lda #5 sta GameMode + ; the destination continent's area byte table was just reloaded from ROM, so + ; reveal the hidden palace now (if that's where we're headed) +RevealHiddenPalaceIfNeeded: + ldy LocationNumber + lda OverworldAreaTable, y + bmi @done + lda HiddenPalaceRevealedAreaUpdate + sta OverworldAreaTable, y +@done: rts @@ -112,84 +131,20 @@ TwisterHitCheck: .reloc TwisterWarp: - ; Direction: OverworldFacingDirection bits - ; next $01=right $08=up - ; previous $02=left $04=down - lda OverworldFacingDirection - ; right ($01) or up ($08) - and #$09 - bne @go_next - lda #$0f ; add $0f (== -1 mod 16) to walk the slot list backwards - bne @scan ; unconditional -@go_next: - lda #$01 ; add 1 to walk the slot list forwards -@scan: - sta R7 - ; scan each of the possible palace global spots in the PalaceMappingTable - lda TwisterCurrentPalaceSlot - ldx #$10 -@next_palace: - clc - adc R7 - ; wrap within the 0-15 global slot space - and #$0f - tay - lda PalaceMappingTable, y - cmp #$ff ; $ff is a place holder indicating no palace at this slot in the world - beq @advance - ; Palace exists here. Check whether its gem has been placed. - sty R6 - ; This table layout was optimized for updating stats (since thats done every frame) - ; and this doesn't matter how long it takes pretty much - sec - sbc #Palace1Offset - ; div by 3 to get the palace number 0 - 6 - sta R5 - lsr - lsr - adc R5 - ror - lsr - adc R5 - ror - lsr - adc R5 - ror - lsr - adc R5 - ror - lsr - ; Check for great palace ID and skip it (palace 6) - cmp #6 - bcs @advance - ; A = CrystalPlaced index 0-5 - tay - lda CrystalPlaced, y - bne @crystalPlacedAtPalace - ; Check next palace slot in the real locations table - ldy R6 -@advance: - tya - dex - bne @next_palace - rts ; this shouldn't happen. tornado should only spawn if a gem is placed -@crystalPlacedAtPalace: - ; R6 = target global slot - lda R6 - sta TwisterCurrentPalaceSlot ; clear the twister slot ldx OverworldCurrentSlot lda #$00 sta OverworldEnemy0Type, x sta OverworldDemonTimer, x ; LocationNumber = $34 + within-region palace code - lda R6 + lda TwisterCurrentPalaceSlot and #$03 clc adc #$34 sta LocationNumber + sta AreaEntranceIndex ; target region = global slot >> 2 - lda R6 + lda TwisterCurrentPalaceSlot lsr lsr cmp RegionNumber @@ -209,7 +164,14 @@ TwisterWarp: ; AreaLoadTrigger does `inc GameMode` and we want mode 0 so set it to $ff lda #$ff sta GameMode + ; the area byte table isn't loaded for the destination continent yet, so the + ; hidden palace reveal is deferred to OverworldPostLoadFluteCheck + bne @load ; unconditional @same_region: + ; still in the destination continent, so its area byte table is already live. + ; reveal the palace destination if we are warping there + jsr RevealHiddenPalaceIfNeeded +@load: pla pla jmp AreaLoadTrigger @@ -217,6 +179,7 @@ TwisterWarp: .reloc RecordPalaceEntry: stx LocationNumber + stx AreaEntranceIndex ; update anchor when Link enters a palace. Palace area indices are $34-$36 ; (within-region palace codes 0-2). Combine with the region for a global slot. txa @@ -237,58 +200,129 @@ RecordPalaceEntry: @done: rts - +; Put the twister spawning into a macro so it can be copied between bank 1 and 2 .macro TwisterSpawnBody -.local crystal_check, crystal_found, active_check, not_active, free_slot, init_slot, spider, doublereturn - ; instruction before was CMP #$0F - beq spider - ; Check if at least one gem is placed ($078D + palace_code, palace_code = 0-5). - ldx #$05 -crystal_check: - lda CrystalPlaced, x - bne crystal_found - dex - bpl crystal_check - bmi doublereturn ; unconditionally exit -crystal_found: ; Refuse to spawn a second twister if one is already active. ldx #$07 -active_check: +@active_check: lda OverworldEnemy0Type, x cmp #$04 ; already have a twister so skip processing it - beq doublereturn + bne @skip + jmp @exit +@skip: dex - bpl active_check - ; Find a free demon slot (type == 0). + bpl @active_check + + ; Check the player direction and find a valid palace to warp to + ; Direction: OverworldFacingDirection bits + ; next $01=right $08=up + ; previous $02=left $04=down + lda OverworldFacingDirection + ; right ($01) or up ($08) + and #$09 + bne @go_next + lda #$ff ; add -1 to walk the list backwards + bne @scan ; unconditional +@go_next: + lda #$01 ; add 1 to walk the slot list forwards +@scan: + sta R7 + + ; skip through the list of crystals placed to find the next palace to go to + ldy TwisterCurrentPalaceSlot + lda PalaceMappingTable,y + jsr @ConvertPalaceIdToMapping + ldx #5 +@next_palace: + clc + adc R7 + and #7 + cmp #6 ; skip over the great palace + bcs @next_palace + tay + lda CrystalPlaced, y + bne @crystalplaced + tya + dex + bpl @next_palace + ; We haven't placed any crystals, so leave + jmp @exit +@crystalplaced: + sty R7 ; use this as a temp storage for the target palace to warp to + ; store the next palace idx that we will warp to + ; now that we know what palace we want to go to, loop through the palace mapping table to find the + ; actual location for it. + ldy #$0f +@next_mapping_entry: + lda PalaceMappingTable, y + cmp #$ff ; $ff is a place holder indicating no palace at this slot in the world + beq @advance + jsr @ConvertPalaceIdToMapping + ; Check if this palace number == the actual palace we are looking for + cmp R7 + beq @foundPalaceToWarpTo + ; haven't found the palace mapping table entry for this palace yet, so loop again +@advance: + dey + bpl @next_mapping_entry + ; we couldn't find the mapping? that doesn't seem possible... + jmp @exit +@foundPalaceToWarpTo: + sty TwisterCurrentPalaceSlot + + ; Find a free encounter slot ldx #$07 -free_slot: +@free_slot: lda OverworldEnemy0Type, x - beq init_slot + beq @init_slot dex - bpl free_slot + bpl @free_slot inx ; set x = 0 and just overwrite slot 0 if all else fails -init_slot: - lda #$04 - sta OverworldEnemy0Type, x ; type 4 = twister +@init_slot: + lda #TWISTER_ENEMY_ID + sta OverworldEnemy0Type, x lda #$40 - sta OverworldDemonTimer, x ; - lda OverworldScrollY ; camera Y + sta OverworldDemonTimer, x + lda OverworldScrollY clc - adc #$6c ; center-screen height - sta OverworldEnemyYPosition, x ; world Y - lda ScrollPosShadow ; camera X - sta OverworldEnemyXPosition, x ; world X = left screen edge (OAM X = 0) + ; center-screen height where the player is + adc #$6c + sta OverworldEnemyYPosition, x + ; spawn it at the left edge of the screen + lda ScrollPosShadow + sta OverworldEnemyXPosition, x + ; horizontally move at 2px right a frame lda #$02 - sta OverworldDemonXVelocity, x ; +2/frame rightward + sta OverworldDemonXVelocity, x lda #$00 - sta OverworldDemonYVelocity, x ; no vertical movement - ; Twister spawned. Return from flute dispatch (don't fall into L838F). -doublereturn: - pla - pla -spider: - rts ; returns to L838F: spider/rock encounter + sta OverworldDemonYVelocity, x +@exit: + ; And also check if its time to do spider/3 eye rock encounter + lda OverworldFacingTerrain + rts +@ConvertPalaceIdToMapping: + ; This table layout was optimized for updating stats (since thats done every frame) + ; and this doesn't matter how long it takes pretty much. So we need to convert + sec + sbc #Palace1Offset + ; div by 3 to get the palace number 0 - 6 + sta R5 + lsr + lsr + adc R5 + ror + lsr + adc R5 + ror + lsr + adc R5 + ror + lsr + adc R5 + ror + lsr + rts .endmacro @@ -299,11 +333,8 @@ spider: jmp $836f FREE_UNTIL $836f -.org $838C +.org $8387 jsr TwisterSpawnPRG1 -; The code after the patch point is where it normally branches to if the flute -; is removing a spider or spawning 3 eyed rock -.assert * = $838f .reloc .proc TwisterSpawnPRG1 @@ -319,9 +350,8 @@ FREE_UNTIL $836f FREE_UNTIL $836f ; Same hook at the same address. -.org $838C +.org $8387 jsr TwisterSpawnPRG2 -.assert * = $838f .reloc .proc TwisterSpawnPRG2 diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index 569a4e2c3..3f0ac3db3 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -62,12 +62,12 @@ zp_15 = $15 zp_16 = $16 LinkStanding = $17 ; 0 = Link in ducked shield position, 1 = Link standing DarkLinkStanding = $18 ; 0 = Dark Link ducking, 1 = Dark Link standing -LinkYPositionHi = $19 ; [SS] 0 = Invisible, 1 = Normal, 2-FF = Fall in hole - [FILE] Cursor position -Enemy0YPositionHi = $1a ; [SS] 0 = Enemy0 off the top of the screen, 1 = Normal - [FILE] Save file presence bits +LinkYPositionHi = $19 ; 0 = Invisible, 1 = Normal, 2-FF = Fall in hole - [FILE] Cursor position +Enemy0YPositionHi = $1a ; 0 = Enemy0 off the top of the screen, 1 = Normal - [FILE] Save file presence bits Enemy1YPositionHi = $1b Enemy2YPositionHi = $1c Enemy3YPositionHi = $1d -Enemy4YPositionHi = $1e ; [SS] 0 = Enemy4 off the top of the screen, 1 = Normal - [FILE] File selection cursor +Enemy4YPositionHi = $1e ; 0 = Enemy4 off the top of the screen, 1 = Normal - [FILE] File selection cursor Enemy5YPositionHi = $1f Enemy0Condition = $20 Enemy1Condition = $21 @@ -81,7 +81,7 @@ zp_28 = $28 LinkYPos = $29 EnemyYPositionLo = $2a Enemy0YPositionLo = $2a -OverworldEnemyYPosition = $2a ; [OW] base of overworld demon Y world position array, indexed by slot X +OverworldEnemyYPosition = $2a ; base of overworld demon Y world position array, indexed by slot X Enemy1YPositionLo = $2b Enemy2YPositionLo = $2c Enemy3YPositionLo = $2d @@ -122,7 +122,7 @@ LinkXPosition = $4d LinkXPositionLo = $4d EnemyXPositionLo = $4e Enemy0XPositionLo = $4e -OverworldEnemyXPosition = $4e ; [OW] base of overworld demon X world position array, indexed by slot X +OverworldEnemyXPosition = $4e ; base of overworld demon X world position array, indexed by slot X Enemy1XPositionLo = $4f Enemy2XPositionLo = $50 Enemy3XPositionLo = $51 @@ -159,10 +159,10 @@ zp_6f = $6f LinkXVelocity = $70 Enemy0XVelocity = $71 Enemy1XVelocity = $72 -OverworldYPosition = $73 ; [SS] Enemy2XVelocity - [OW] OverworldYPosition -OverworldXPosition = $74 ; [SS] Enemy3XVelocity - [OW] OverworldXPosition -OverworldLinkYPosition = $75 ; [SS] Enemy4XVelocity - [OW] OverworldLinkYPosition -OverworldLinkXPosition = $76 ; [SS] Enemy5XVelocity - [OW] OverworldLinkXPosition +OverworldYPosition = $73 ; Enemy2XVelocity / OverworldYPosition +OverworldXPosition = $74 ; Enemy3XVelocity / OverworldXPosition +OverworldLinkYPosition = $75 ; Enemy4XVelocity / OverworldLinkYPosition +OverworldLinkXPosition = $76 ; Enemy5XVelocity / OverworldLinkXPosition Projectile0XVelocity = $77 Projectile1XVelocity = $78 Projectile2XVelocity = $79 @@ -172,20 +172,20 @@ Projectile5XVelocity = $7c LinkProjectile0XVelocity = $7d LinkProjectile1XVelocity = $7e zp_7f = $7f -OverworldScrollY = $7f ; [OW] camera Y scroll offset; OAM_Y = world_Y - OverworldScrollY +OverworldScrollY = $7f ; camera Y scroll offset; screen Y = world_Y - OverworldScrollY LinkAnimationFrame = $80 -OverworldCurrentSlot = $80 ; [OW] current overworld demon slot index during per-frame processing (0-7) +OverworldCurrentSlot = $80 ; current overworld demon slot index during per-frame processing (0-7) Enemy0AnimationFrame = $81 -OverworldEnemy0Type = $82 ; [SS] Enemy1AnimationFrame - [OW] OverworldEnemy0Type, 1 = Weak, 2 = Strong, 3 = Fairy -OverworldEnemy1Type = $83 ; [SS] Enemy2AnimationFrame - [OW] OverworldEnemy1Type -OverworldEnemy2Type = $84 ; [SS] Enemy3AnimationFrame - [OW] OverworldEnemy2Type -OverworldEnemy3Type = $85 ; [SS] Enemy4AnimationFrame - [OW] OverworldEnemy3Type -OverworldEnemy4Type = $86 ; [SS] Enemy5AnimationFrame - [OW] OverworldEnemy4Type +OverworldEnemy0Type = $82 ; Enemy1AnimationFrame / OverworldEnemy0Type, 1 = Weak, 2 = Strong, 3 = Fairy +OverworldEnemy1Type = $83 ; Enemy2AnimationFrame / OverworldEnemy1Type +OverworldEnemy2Type = $84 ; Enemy3AnimationFrame / OverworldEnemy2Type +OverworldEnemy3Type = $85 ; Enemy4AnimationFrame / OverworldEnemy3Type +OverworldEnemy4Type = $86 ; Enemy5AnimationFrame / OverworldEnemy4Type ProjectileType = $87 Enemy_Flag = ProjectileType -Projectile0Type = $87 ; [SS] Projectile0Type - [OW] OverworldEnemy5Type -Projectile1Type = $88 ; [SS] Projectile1Type - [OW] OverworldEnemy6Type -Projectile2Type = $89 ; [SS] Projectile2Type - [OW] OverworldEnemy7Type +Projectile0Type = $87 ; Projectile0Type / OverworldEnemy5Type +Projectile1Type = $88 ; Projectile1Type / OverworldEnemy6Type +Projectile2Type = $89 ; Projectile2Type / OverworldEnemy7Type Projectile3Type = $8a Projectile4Type = $8b Projectile5Type = $8c @@ -340,17 +340,17 @@ timer_501 = $0501 ; $501 through $50d are timers that are LinkEntrySlideTime = $0503 SwordAttackTimer = $050a InjuryTimer = $050c -OverworldDemonTimer = $050e ; [OW] overworld demon active flag/timer, indexed by slot X; non-zero = active +OverworldDemonTimer = $050e ; overworld demon active flag/timer, indexed by slot X; non-zero = active EncounterSpawnTimer = $0516 RNG = $051b MapNumber = $0561 -OverworldFacingDirection = $0562 ; [OW] Link's facing direction: $01=right, $02=left, $04=down, $08=up -OverworldFacingTerrain = $0563 ; [OW] terrain type Link is currently facing +OverworldFacingDirection = $0562 ; Link's facing direction: $01=right, $02=left, $04=down, $08=up +OverworldFacingTerrain = $0563 ; terrain type Link is currently facing TownNumber = $056b PalaceNumber = $056c PalaceRegionIndex = $056c ; Palace index in continent (0-2) -OverworldDemonYVelocity = $056d ; [OW] overworld demon Y velocity, indexed by slot X (signed: $01=down, $FF=up) -OverworldDemonXVelocity = $0575 ; [OW] overworld demon X velocity, indexed by slot X (signed: $01=right, $FF=left) +OverworldDemonYVelocity = $056d ; overworld demon Y velocity, indexed by slot X (signed: $01=down, $FF=up) +OverworldDemonXVelocity = $0575 ; overworld demon X velocity, indexed by slot X (signed: $01=right, $FF=left) LinkYVelocityHi = $057d EnemyYVelocity = $057e Enemy0YVelocity = $057e @@ -467,6 +467,10 @@ SavedDoorExit1ScrollX = $69b1 ; accessed as $69ac,X with X = 5 SavedDoorExit2ScrollPage = $69ab ; used for New Kasuto fireplace SavedDoorExit2ScrollX = $69b2 +; LocationNumber will later be changed to the side-scrolling location +; index. z2ft needs this, at this specific address. +AreaEntranceIndex = $69ff + ; All Timestamp types listed below for reference TS_COUNT .set 0 .macro TimestampId name @@ -517,8 +521,10 @@ RESV BlockBreakColumnOffset, BREAKABLE_BLOCK_COUNT ; Vanilla addr $042E - Offset RESV BlockBreakAddrLo, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table lo addr RESV BlockBreakAddrHi, BREAKABLE_BLOCK_COUNT ; Vanilla addr $0433 - Collision table hi addr -RESV TwisterCurrentPalaceSlot ; global palace slot (RegionNumber*4 + within-region palace code) of last dungeon Link entered or warped to -RESV TwisterPendingPalaceLocation ; flute cross-continent warp: non-zero = palace LocationNumber to enter once the destination region's overworld has been reloaded +; global palace slot of last dungeon Link entered or warped to +RESV TwisterCurrentPalaceSlot +; Nonzero if we are changing continents with the warp and need to reload it +RESV TwisterPendingPalaceLocation .ifdef ENABLE_Z2_MARIO diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index e0e64ac05..0060b537e 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -246,6 +246,16 @@ PatchGoingDownChimney: .org $e07f jmp *+3 +; Patch the flute sound to use the mario 3 music instead +; -- not happening right now. SMB3 flute is MUSIC instead of sfx, and i aint porting +; that right now. Music data is bank 28 at $a990 (M12ASegData03 in the disassembly) +; .segment "PRG0" +; .org $851d +; lda #Sfx_EnemySmack +; sta Square1SoundQueue + +.segment "PRG7" + .org $E1A2 jsr CheckIfAboveScreenForFallingFairyCheck nop diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s index ed67d51c9..2aea62abb 100644 --- a/RandomizerCore/Asm/z2mario/sfx.s +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -144,6 +144,12 @@ SwimStompEnvelopeData: ; $01-$7F - note index ($7E is "rest") ; $80-$FF - CTL1 latch, does NOT consume a new note this frame ; ----------------------------------------------------------------------------- +PlaySMB3Flute: + lda #SMB3FluteSong - SndLev1_DataLongWag + sta Squ1_SfxLenCounter + lda #$90 + sta TailWag_CtlHold + jmp ContinueTailWag PlayTailWag: lda #$00 sta Squ1_SfxLenCounter @@ -281,7 +287,9 @@ Square1SfxHandler: lsr Square1SoundQueue bcs PlaySwimStomp ;swim/stomp lsr Square1SoundQueue - bcs PlaySmackEnemy ;smack enemy + bcc + + jmp PlaySMB3Flute + + ; was PlaySmackEnemy ;smack enemy lsr Square1SoundQueue bcc + jmp PlayPipeDownInj ;pipedown/injury @@ -685,4 +693,11 @@ FreqRegLookupTbl: SndLev1_DataLongWag: .byte $90, $7E, $7E, $97 ; $A5BB - $A5CA .byte $4C, $4E, $90, $7E, $7E, $95, $52, $54, $56, $58, $94, $52, $54, $56, $58, $93 ; $A5CB - $A5DA - .byte $52, $54, $56, $58, $00 \ No newline at end of file + .byte $52, $54, $56, $58, $00 + +SMB3FluteSong: + .byte $98, $7E, $28, $22, $7E, $98, $20 ; $ACF9 - $AD08 + .byte $94, $20, $22, $88, $7E, $7E, $00, $98, $7E, $1E, $18, $7E, $98, $16, $94, $16 ; $AD09 - $AD18 + .byte $18, $88, $7E, $7E, $A8, $3A, $84, $7E, $88, $3A, $A4, $30, $36, $38, $A8, $3A ; $AD19 - $AD28 + .byte $84, $7E, $88, $3A, $A4, $30, $36, $38, $94, $04, $02, $00, $94, $0A, $0B, $07 ; $AD29 - $AD38 + .byte $07, $00 diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index d866ff5d2..ac8e89721 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3657,17 +3657,13 @@ ldy RegionNumber ; This is currently true for vanilla and Z2R RAFT_TILE_INDEX = $29 -; LocationNumber will later be changed to the side-scrolling location -; index. z2ft needs this, at this specific address. -AREA_ENTRANCE_INDEX = $69ff - .org $8528 ; we don't use this data anymore FREE_UNTIL $8553 ; remove unused data here ; bridge connector coordinates are at $8553 .org $8599 stx LocationNumber ; X stashed away like in the original code - stx AREA_ENTRANCE_INDEX + stx AreaEntranceIndex cpx #RAFT_TILE_INDEX bne NotRaftTileProceed lda HaveRaft From ad65903204494ab43cfb1ee92fc42d197beb03ae Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 13 Jul 2026 00:55:53 -0400 Subject: [PATCH 095/128] Play 1up sound when grabbing a link doll --- RandomizerCore/Asm/z2mario/integration.s | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 0060b537e..a191cb861 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1698,6 +1698,20 @@ CheckIfPlayerSmall: .byte $23,$14,$0b,$ee,$e7,$e5,$e2,$e4,$de,$f4,$e5,$e2,$e7,$e4,$ff .assert * = $8f9e +.segment "PRG7" +; play the 1-up sound from mario instead of from zelda when grabbing a link doll +.org $E818 + jsr PlayMario1UpSfx + +.reloc +PlayMario1UpSfx: + inc NumberofLives + lda #0 + sta $eb ; cancel the regular z2 music change + lda #Sfx_ExtraLife + sta Square2SoundQueue ;and play the 1-up sound + rts + ; The following are locations where it draws the explosion ; after killing a boss From e0f32a0dc97dd549af46ccdb68c946fb6d63c77f Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 13 Jul 2026 01:23:00 -0400 Subject: [PATCH 096/128] Make small mario throw hammers but use a nonglitchy frame unless fire --- RandomizerCore/Asm/z2mario/mario.s | 37 +++++++++++++++++++----------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 6886b0654..3a8122b01 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -442,21 +442,32 @@ PlayerGfxProcessing: sta FireballThrowingTimer ;otherwise store animation timer into fireball throw timer ; Change the Mario sprite to the fire version - lda ObjectMetasprite - clc - adc #FIRE_MARIO_OFFSET - sta ObjectMetasprite + ; but only if fire is cast or you are big mario + lda PlayerSize + eor #1 + ora FireSpellActive + beq NotSmallFireProbably + lda ObjectMetasprite + clc + adc #FIRE_MARIO_OFFSET + sta ObjectMetasprite +NotSmallFireProbably: lda Player_X_Speed ora Left_Right_Buttons ;check for horizontal speed or left/right button press bne SUpdR ;if no speed or button press, branch using set value in Y - ; Use the glitchy version of the sprite + ; Use the glitchy version of the sprite if you are small and fire mario lda PlayerSize bne SmallFireMario lda #METASPRITE_FIRE_MARIO_SWIMMING_STILL_1 bne SetFrame SmallFireMario: - lda #METASPRITE_SMALL_FIRE_SWIMMING_STILL_1 + lda FireSpellActive + beq SmallOnlyProbablyHammer + lda #METASPRITE_SMALL_FIRE_SWIMMING_STILL_1 + bne SetFrame + SmallOnlyProbablyHammer: + lda #METASPRITE_SMALL_MARIO_SWIMMING_1_KICK SetFrame: sta ObjectMetasprite SUpdR: @@ -2511,13 +2522,13 @@ GetProperObjOffset: jmp UpdateHammers + ; Check if you either have upstab - lda PlayerSize ; or are big enough to throw hammers - beq + - lda HaveStabs - and #HAVE_UPSTAB - bne + - jmp UpdateHammers - + + ; lda PlayerSize ; or are big enough to throw hammers + ; beq + + ; lda HaveStabs + ; and #HAVE_UPSTAB + ; bne + + ; jmp UpdateHammers + ; + ; and that the player is pushing up + b lda SavedJoypadBits and #B_Button | Up_Dir ;check for up + b button pressed From fe46958a1cba4dfd9db7ebc8d60e9646ebe909f3 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 13 Jul 2026 13:21:57 -0400 Subject: [PATCH 097/128] Skip over upstab hitbox check which triggered on marios swimming frame --- RandomizerCore/Asm/z2mario/integration.s | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index a191cb861..73dd22a54 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1518,10 +1518,11 @@ SetDownstabStatTracking: @Exit: rts -.org $E9C0 +; This check for upstab/downstab and we can just check for downstab in mario instead +.org $e9ba jsr InyIfDownstabbing - nop - nop + jmp $e9c5 +FREE_UNTIL $e9c5 .reloc InyIfDownstabbing: From 59231c7e0f083ccefcdb756584a27579768c06b2 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 13 Jul 2026 14:40:45 -0400 Subject: [PATCH 098/128] Fix fairy bouncing phasing through sometimes --- RandomizerCore/Asm/z2mario/integration.s | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 73dd22a54..9e136deba 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1507,6 +1507,14 @@ SkipEnemyRecoilIfThrowingHammer: @skip: .assert * = $E723 +; During fairy-swim Mario's Link animation frame ($80) sits at $08, which vanilla +; reads as an up-stab. The up-stab special-case at $E6F3 then discards any hit where +; Link is above the enemy (LDA $29; CMP $2A,x; BCC no-hit) which causes the fairy stomp +; to just phase through enemies sometimes. Just skip this dumb check >:( +.org $E6F3 + jmp $E708 +FREE_UNTIL $E708 + .reloc SetDownstabStatTracking: ;set recoil to -4 to bounce up From f323924d6cd86331108aa880f9a075e5552c2f3d Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 13 Jul 2026 19:08:17 -0400 Subject: [PATCH 099/128] Clear out some mario specific code on screen transition --- RandomizerCore/Asm/z2mario/integration.s | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 9e136deba..96ed213bd 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1647,9 +1647,13 @@ DrawMapMario: jsr ClearExtraRAMOnScreenTransition .reloc ClearExtraRAMOnScreenTransition: - ; original patched code lda #0 - tax + ; Run through the stack ram and clear some stuff. + ldx #CrouchingFlag - CurrentOAMOffset +- + sta CurrentOAMOffset, x + dex + bne - ; also clear custom projectile state sta Fireball_State+0 sta Fireball_State+1 From bd84f3665888a60b1ae8489eaaa2c2d33f9f96e5 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 13 Jul 2026 21:23:22 -0400 Subject: [PATCH 100/128] remove unused palace tracking which mistakingly overwrote the raft patch --- RandomizerCore/Asm/Flute.s | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/RandomizerCore/Asm/Flute.s b/RandomizerCore/Asm/Flute.s index 1aac6e855..6d90bf714 100644 --- a/RandomizerCore/Asm/Flute.s +++ b/RandomizerCore/Asm/Flute.s @@ -35,9 +35,6 @@ OverworldPostLoad = $C72F .org $83CE jsr TwisterHitCheck -.org $85A9 - jsr RecordPalaceEntry - ; When taking a cross continent flute, we load the overworld and then the dungeon. ; Hook right before we reveal the overworld and teleport to the dungeon .org OverworldPostLoad @@ -176,30 +173,6 @@ TwisterWarp: pla jmp AreaLoadTrigger -.reloc -RecordPalaceEntry: - stx LocationNumber - stx AreaEntranceIndex - ; update anchor when Link enters a palace. Palace area indices are $34-$36 - ; (within-region palace codes 0-2). Combine with the region for a global slot. - txa - sec - sbc #$34 - bcc @done ; below $34: not a palace - cmp #$03 - bcs @done ; $37+: not a palace entrance - ; A = within-region palace code (0-2); global slot = region*4 + code - pha - lda RegionNumber - asl - asl - sta TwisterCurrentPalaceSlot - pla - ora TwisterCurrentPalaceSlot - sta TwisterCurrentPalaceSlot -@done: - rts - ; Put the twister spawning into a macro so it can be copied between bank 1 and 2 .macro TwisterSpawnBody ; Refuse to spawn a second twister if one is already active. From 0b8afd13671d2b5dbf79f97c7006f5bc16bf5bf0 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 13 Jul 2026 21:25:04 -0400 Subject: [PATCH 101/128] Update the dark mario fight to let you use all attacks --- RandomizerCore/Asm/z2mario/boss.s | 261 ++++++++++++++++++++--------- RandomizerCore/Asm/z2mario/mario.s | 8 +- 2 files changed, 185 insertions(+), 84 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index 2693e4768..13cb149c7 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -9,8 +9,6 @@ .import METASPRITE_FIREBALL_FRAME_1, METASPRITE_FIREBALL_FRAME_2 .import METASPRITE_HAMMER_FRAME_1, METASPRITE_HAMMER_FRAME_2 -.export InjuredBoss - .import METASPRITE_BIG_MARIO_CROUCHING CROUCHING = METASPRITE_BIG_MARIO_CROUCHING SKIDDING = METASPRITE_BIG_MARIO_SKIDDING @@ -47,6 +45,11 @@ RIGHT_EDGE_TARGET = 10*16 BOSS_MAX_RUN_SPEED = $50 BOSS_DECEL_DISTANCE = 3 * 16 ; in metatiles +; Damage for stomping/hammer/tail swing +BOSS_HP_INIT = 80 +STOMP_DAMAGE = 5 +HAMMER_DAMAGE = 5 +FIREBALL_DAMAGE = 1 .segment "PRG7" ; patch the vanilla enemy projectile check to prevent it from trying to run our hammer/fireball @@ -91,6 +94,7 @@ BossLandedInCutscene: sta $2a ; force the boss to land at the right height on the ground lda #2 sta Enemy_MovingDir ; force the boss to start by facing left + sta EnemyFacingDir rts .segment "PRG5" @@ -116,7 +120,7 @@ InitShadowBoss: bpl @loop ldx #0 ; Dark Link entity is always at enemy slot 0 ; Set HP - lda #60 + lda #BOSS_HP_INIT sta EnemyHP,x lda #JUMPING sta boss_animation @@ -128,6 +132,7 @@ InitShadowBoss: ldx #0 lda #2 sta Enemy_MovingDir,x + sta EnemyFacingDir,x rts @@ -181,8 +186,8 @@ RunBoss: lda #0 sta $de ; unfreeze link - ; Display boss HP bar with a divisor of 7 (close enough to 60 HP) - lda #7 + ; Display boss HP bar with a divisor of 10 + lda #80 / 8 jsr $A4E9 lda boss_freeze @@ -201,6 +206,10 @@ RunBoss: rts @notfreezed: + ; keep player projectiles running even while the boss is dying + ldx #0 + jsr BossUpdateProjectiles + lda EnemyHP,x bmi AnimateKilledBoss ; HP < 0: boss is fully dead bne @alive @@ -224,8 +233,6 @@ RunBoss: @nodraw: ldx #0 - jsr BossUpdateProjectiles - lda TimerControl bne @nograv ; skip movement if timer frozen @@ -240,16 +247,17 @@ RunBoss: lda #JUMPING sta boss_animation @nograv: - - ; Collision detection (only when not invincible). While invincible: - ; 1. Skip BossPlayerCollision entirely so the boss body cannot register - ; either a stomp re-hit or contact damage on Mario. - ; 2. Force-clear bit 4 of $A8,x (the enemy-contacted-link flag) so any - ; contact that registered right before the boss took damage can't - ; fire bank7_LinkCollision later in the frame. lda boss_invincibility_timer bne @decrement_timer - jmp BossPlayerCollision + jsr BossWeaponCollision + ; If a weapon hit just landed, boss_invincibility_timer was just set. + ; Skip BossPlayerCollision so the same frame can't also register body + ; contact. + lda boss_invincibility_timer + bne @done + jmp BossPlayerCollision + @done: + rts @decrement_timer: lda $a8,x @@ -471,6 +479,7 @@ MovChangeState: lda Enemy_MovingDir,x eor #%00000011 sta Enemy_MovingDir,x + sta EnemyFacingDir,x rts .reloc @@ -717,12 +726,19 @@ BossGraphicsHandlerForceDraw: lsr bcs @restore @draw: + ; boss is offset by 8 px and i dunno how much i care to fix it? + lda SprObject_X_Position+1 + pha + sec + sbc #8 + sta SprObject_X_Position+1 lda #2 ; palette 3 attribute (shadow mario palette) sta SprObject_SprAttrib,y ldx boss_animation jsr DrawMetasprite jsr BossDrawProjectiles ; shadow fireballs + hammers (palette already set per-slot) - + pla + sta SprObject_X_Position+1 @restore: pla sta NmiBankShadowA @@ -1031,12 +1047,6 @@ KillBoss: sta Enemy_Y_Speed,x lda #0 sta Enemy_X_Speed,x - sta EnemyProjectileType+0 - sta EnemyProjectileType+1 - sta EnemyProjectileType+2 - sta EnemyProjectileType+3 - sta EnemyProjectileType+4 - sta EnemyProjectileType+5 lda #$7f sta boss_counter ; vanilla boss kill behavior @@ -1063,31 +1073,6 @@ KillBoss: rts .reloc -StompedBoss: - lda Player_Y_Position - clc - adc #10 - cmp Enemy_Y_Position,x - bcc @valid_stomp - jmp DamagePlayer - @valid_stomp: - lda #$fd - sta Player_Y_Speed ; bounce player upward - lda boss_state - bne InjuredBoss ; if not in state 0, just take damage - lda #0 - sta boss_state_timer ; if in idle state, also reset timer - -InjuredBoss: - lda boss_invincibility_timer - bne @done ; already invincible, no damage - inc boss_screech ; track hit count - dec EnemyHP,x ; reduce HP - lda #80 - sta boss_invincibility_timer ; 80 frames of invincibility - @done: - rts -.reloc DamagePlayer: lda $a8,x ora #$10 ; set flag indicating link was hit by this enemy? @@ -1095,31 +1080,27 @@ DamagePlayer: rts ; Vanilla hitbox primitive addresses (all in fixed PRG7, callable from anywhere) -bank7_LoadObjectHitbox = $E942 ; loads enemy hitbox into ZP $04-$07 using Enemy_X/Y_Position,x +bank7_LoadObjectHitbox = $E942 ; loads enemy hitbox into ZP $04-$07 using Enemy_X/Y_Position bank7_LoadLinkHitbox = $E975 ; loads Link/Mario body hitbox into ZP $00-$03 -bank7_CollisionTest = $E9F9 ; rectangle intersection; Carry set = overlap -bank7_LinkCollision = $D6C1 ; processes $A8,x enemy-hit flags -> calls link hurt routine +bank7_CollisionTest = $E9F9 ; rectangle intersection check +bank7_LinkCollision = $D6C1 ; processes $A8,x enemy-hit flags and calls link hurt routine to damage you .reloc ; Check contact between Mario and the boss each frame. BossPlayerCollision: ldx #0 - - jsr bank7_LoadObjectHitbox ; boss body hitbox into ZP $04-$07 - - ; Stomp detection: Mario must be falling (Player_Y_Speed > 0 in SMB1 convention). - ; $00 = on the ground, $80-$FF = ascending, $01-$7F = falling. + jsr bank7_LoadObjectHitbox + ; Run the regular stomp detection code because too lazy to hook the right spot which + ; would've already had all this crap :p lda Player_Y_Speed - beq @try_body_contact ; standing – not a stomp - bmi @try_body_contact ; going up – not a stomp - ; Mario is falling: check body overlap for stomp - jsr bank7_LoadLinkHitbox ; Mario body hitbox into ZP $00-$03 - jsr bank7_CollisionTest ; carry set = overlap + beq @try_body_contact + bmi @try_body_contact + ; Mario is falling so check if we overlap + jsr bank7_LoadLinkHitbox + jsr bank7_CollisionTest bcc @no_contact - jsr InjuredBossJump - lda #$fd - sta Player_Y_Speed ; bounce Mario up - rts - + lda #$fd + sta Player_Y_Speed ; bounce Mario up + jmp InjuredBossJump @try_body_contact: jsr bank7_LoadLinkHitbox ; Mario body hitbox into ZP $00-$03 jsr bank7_CollisionTest @@ -1130,42 +1111,156 @@ BossPlayerCollision: @contact_damage: ; Mario touched boss without stomping: hurt Mario via vanilla damage system lda $a8,x - ora #$10 ; enemy-contacted-link flag + ora #$10 sta $a8,x - jmp bank7_LinkCollision ; processes $A8 flag to damage Mario; returns to RunBoss caller + jmp bank7_LinkCollision .reloc -; Variant of InjuredBoss that subtracts 5 HP at once (jump attack damage). -; HP is floored at 0 so the KillBoss death-countdown path runs normally. -InjuredBossJump: +BossWeaponCollision: + ldy #0 +@proj_loop: + sty $11 + lda Fireball_State,y + beq @proj_next + and #%10000000 + bne @proj_next + ; we have an actual fireball thats still running + lda Fireball_X_Position,y + sec + sbc ScreenLeft_X_Pos + sta $00 + lda Fireball_PageLoc,y + sbc ScreenLeft_PageLoc + bne @proj_next + ; and its not offscreen so load its collision + lda Fireball_Y_Position,y + clc + adc #4 + sta $01 + lda #8 + sta $02 + sta $03 + + ldx #0 + jsr bank7_LoadObjectHitbox + jsr bank7_CollisionTest + ldx #0 + ldy $11 + bcc @proj_next + ; if it hit the boss + + lda Fireball_State,y + and #%01000000 + beq @proj_fireball + ; and its a hammer deal 5 + lda #HAMMER_DAMAGE + jsr BossTakeDamage + jmp @proj_next +@proj_fireball: + ; fireballs apply 1 damage but don't trigger invincibility frames + lda #FIREBALL_DAMAGE + jsr BossApplyDamage + ; and then blow up + lda #%10000000 + sta Fireball_State,y +@proj_next: + ldy $11 + iny + cpy #2 + bcc @proj_loop + + ; check if hitting with the tanooki tail (copy pastaed from the other place because lazy) + lda $d0 + beq @done + lda TailSwingTimer + cmp #9 + bcc @done + cmp #13 + bcs @done + + lda Player_Y_Position + clc + adc #$10 + sta $01 + lda PlayerFacingDir + cmp #1 + bne @faceLeft + lda LinkXScreenPosition + clc + adc #30 + jmp @writeX +@faceLeft: + lda LinkXScreenPosition + sec + sbc #10 +@writeX: + sta $00 + lda #$0E + sta $02 + lda #$08 + sta $03 + + ldx #0 + jsr bank7_LoadObjectHitbox + jsr bank7_CollisionTest + ldx #0 + bcc @done + ; Tail swipes deal 5 damage and act like a stomp + jmp InjuredBossJump +@done: + rts + +.reloc +; Core damage code that runs for all types of damage +BossApplyDamage: + sta $00 lda boss_invincibility_timer - bne @done ; already invincible, no damage (caller still bounces Mario) + beq + + clc + rts ++ inc boss_screech lda EnemyHP,x sec - sbc #5 + sbc $00 bcs + lda #0 ; floor at 0 + sta EnemyHP,x beq + - ; don't play sfx for last hit. works around a weird bug i didn't bother to fix - lda #$10 ; Sword stab SFX - sta Z2NoiseSoundQueue ; stab SFX + ; play stab sfx except for last hit. works around a weird bug i didn't bother to fix + lda #$10 + sta Z2NoiseSoundQueue + - lda #60 ; 1 second of invincibility at 60 fps - sta boss_invincibility_timer - ; A successful stomp should also short-circuit any standing pause the boss - ; was in - if the player can land a hit, the boss should react / counter - ; immediately rather than continuing to wind down from the previous state. + sec + rts + +.reloc +; Applies damage and also sets iframes for the boss +BossTakeDamage: + jsr BossApplyDamage + bcc @done + lda #60 + sta boss_invincibility_timer + sec +@done: + rts + +.reloc +; Deals 5 damage and also cancels the current action the boss is taking +InjuredBossJump: + lda #STOMP_DAMAGE + jsr BossTakeDamage + bcc @done + ; We hit the boss, so reduce the transition delay 0 to make the boss get going immediately lda #0 sta boss_transition_delay - ; Reset state timer if currently idle (mirrors InjuredBoss behavior) + ; and clears the boss state timer if we are doing something lda boss_state bne @done - sta boss_state_timer ; A is already 0 here + sta boss_state_timer @done: - rts + rts .segment "PRG7" .reloc diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 3a8122b01..bbde4cae5 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -1951,7 +1951,12 @@ BHalf: bne SideCheckLoop ;run code until both sides of player are checked ExSCH: rts ;leave - +CheckForBreakableBlock: + cmp #$4c + bne @skip +@skip: + clc + rts CheckSideMTiles: ; for now just always block movement jmp StopPlayerMove @@ -2159,6 +2164,7 @@ PlayerHeadCollision: ; eor #$01 ;and floatey numbers ; sta SprDataOffset_Ctrl + lda #$00 sta Player_Y_Speed ;init player's vertical speed ExitFireballNearby: From 8100e45881d760e28b55822db894b56cd8ed697f Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 14 Jul 2026 13:13:29 -0400 Subject: [PATCH 102/128] Allow big mario to break bricks with his head to make p1 easier --- RandomizerCore/Asm/z2mario/integration.s | 9 +- RandomizerCore/Asm/z2mario/mario.s | 151 +++++++++-------------- RandomizerCore/Asm/z2r.inc | 4 + 3 files changed, 71 insertions(+), 93 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 96ed213bd..c93d0336d 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1029,6 +1029,10 @@ SetTailSwingHitbox: .segment "PRG7" +; Make keys hitboxes wider to reduce some jank in mario +.org $e936 + .byte $00, $0D, $08, $07 + LoadProjectileCollisionBox = $e4bc SwordCollisionCheck = $E677 BreakBlockCollisionCheck = $e1e6 @@ -1209,9 +1213,11 @@ AddSariaBridgeToMarioCollision: .org $A700 jmp ClearSpotInCollisionRam .reloc -.proc ClearSpotInCollisionRam +.export ClearSpotInCollisionRamJustOneSpot +ClearSpotInCollisionRam: ; A is always #$42 here? sta ($00),y +ClearSpotInCollisionRamJustOneSpot: lda $00 clc adc #<(COLLISION_TILES - $6000) @@ -1222,7 +1228,6 @@ AddSariaBridgeToMarioCollision: lda #0 ; Clear out the collision tile sta ($00),y rts -.endproc ; Patch clearing out the ram for sideviews to also clear our new ram block .org $C58F diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index bbde4cae5..595d375ea 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -1825,7 +1825,7 @@ HeadChk: cpy #$04 ;from collision detection routine bcc DoFootCheck ;if low nybble < 4, branch ; we can't check solid type here since its banked out - ; jsr CheckForSolidMTiles ;check to see what player's head bumped on + jsr CheckForNonBreakableBlocks ;check to see what player's head bumped on bcs SolidOrClimb ;if player collided with solid metatile, branch ; ldy AreaType ;otherwise check area type ; beq NYSpd ;if water level, branch ahead @@ -1951,16 +1951,19 @@ BHalf: bne SideCheckLoop ;run code until both sides of player are checked ExSCH: rts ;leave -CheckForBreakableBlock: - cmp #$4c - bne @skip -@skip: - clc - rts CheckSideMTiles: ; for now just always block movement jmp StopPlayerMove +CheckForNonBreakableBlocks: + ldx CurrentPrgBank + cmp BankSpecificBreakableBlock-4,x + beq @Breakable + sec + rts +@Breakable: + clc + rts ; jsr ChkInvisibleMTiles ;check for hidden or coin 1-up blocks ; beq ExSCH ;branch to leave if either found ; jsr CheckForClimbMTiles ;check for climbable metatiles @@ -2081,95 +2084,61 @@ ExIPM: ;$05 - used to store metatile stored in A at beginning of PlayerHeadCollision ;$06-$07 - used as block buffer address indirect .reloc -BlockYPosAdderData: -; Big, Small - .byte $04, $12 +bank7_Generic_Collision_Test_with_Level_Objects = $EAE8 +bank7_stab_brick_at_0E_with_A_and_does_draw = $E23A +bank7_upstab_downstab_brick_break = $E1F7 +BankSpecificBreakableBlock: + .byte $4c, $4e PlayerHeadCollision: - ; pha ;store metatile number to stack -; lda #$11 ;load unbreakable block object state by default -; ldx SprDataOffset_Ctrl ;load offset control bit here -; ldy PlayerSize ;check player's size -; bne :+ ;if small, branch -; lda #$12 ;otherwise load breakable block object state -; : -; sta Block_State,x ;store into block object buffer - ; farcall DestroyBlockMetatile ;store blank metatile in vram buffer to write to name table - ; ldx SprDataOffset_Ctrl ;load offset control bit - ; lda R2 ;get vertical high nybble offset used in block buffer routine - ; sta Block_Orig_YPos,x ;set as vertical coordinate for block object - ; tay - ; lda R6 ;get low byte of block buffer address used in same routine - ; sta Block_BBuf_Low,x ;save as offset here to be used later - ; lda (R6),y ;get contents of block buffer at old address at $06, $07 - ; jsr BlockBumpedChk ;do a sub to check which block player bumped head on - ; sta R0 ;store metatile here -; ldy PlayerSize ;check player's size -; bne :+ ;if small, use metatile itself as contents of A -; tya ;otherwise init A (note: big = 0) -; : -; bcc PutMTileB ;if no match was found in previous sub, skip ahead -; ; ldy #$11 ;otherwise load unbreakable state into block object buffer -; ; sty Block_State,x ;note this applies to both player sizes -; lda #$c4 ;load empty block metatile into A for now -; ldy R0 ;get metatile from before -; cpy #$58 ;is it brick with coins (with line)? -; beq StartBTmr ;if so, branch -; cpy #$5d ;is it brick with coins (without line)? -; bne PutMTileB ;if not, branch ahead to store empty block metatile -; StartBTmr: -; lda BrickCoinTimerFlag ;check brick coin timer flag -; bne :+ ;if set, timer expired or counting down, thus branch -; lda #$0b -; sta BrickCoinTimer ;if not set, set brick coin timer -; inc BrickCoinTimerFlag ;and set flag linked to it -; : -; lda BrickCoinTimer ;check brick coin timer -; bne :+ ;if not yet expired, branch to use current metatile -; ldy #$c4 ;otherwise use empty block metatile -; : -; tya ;put metatile into A -; PutMTileB: -; sta Block_Metatile,x ;store whatever metatile be appropriate here -; jsr InitBlock_XY_Pos ;get block object horizontal coordinates saved -; ldy R2 ;get vertical high nybble offset -; lda #$23 -; sta (R6),y ;write blank metatile $23 to block buffer -; lda #$10 -; sta BlockBounceTimer ;set block bounce timer -; pla ;pull original metatile from stack -; sta R5 ;and save here -; ldy #$00 ;set default offset -; lda CrouchingFlag ;is player crouching? -; bne SmallBP ;if so, branch to increment offset -; lda PlayerSize ;is player big? -; beq BigBP ;if so, branch to use default offset -; SmallBP: -; iny ;increment for small or big and crouching -; BigBP: -; lda Player_Y_Position ;get player's vertical coordinate -; clc -; adc BlockYPosAdderData,y ;add value determined by size -; and #$f0 ;mask out low nybble to get 16-pixel correspondence -; sta Block_Y_Position,x ;save as vertical coordinate for block object -; ldy Block_State,x ;get block object state -; cpy #$11 -; beq :+ ;if set to value loaded for unbreakable, branch -; jsr BrickShatter ;execute code for breakable brick -; jmp InvOBit ;skip subroutine to do last part of code here -; : -; jsr BumpBlock ;execute code for unbreakable brick or question block -; InvOBit: - ; lda SprDataOffset_Ctrl ;invert control bit used by block objects - ; eor #$01 ;and floatey numbers - ; sta SprDataOffset_Ctrl - - - lda #$00 + ; small can't break with head + lda PlayerSize + bne ExitBackToBump + + ; put a hitbox check for the player right above the head... i guess? + lda LinkXScreenPosition + clc + adc #$0a + ldy LinkFacingDirection + dey + adc $e1db,y + clc + adc #$04 + adc ScrollX + sta LinkProjectileXPositionLoRelated + lda ScrollPage + adc #$00 + sta LinkProjectileXPositionHiRelated + + lda CrouchingFlag ; 4 if crouching + asl + asl + adc Player_Y_Position + sta LinkProjectileYPositionRelated + ldx #$10 ; use the hitboxes we just set + ldy #$1c ; tell it we are stabbing? + jsr bank7_Generic_Collision_Test_with_Level_Objects + ldy CurrentPrgBank + cmp BankSpecificBreakableBlock-4,y + bne ExitBackToBump + lda $e + clc + adc #<(COLLISION_TILES - $6000) + sta $c + lda $f + adc #>(COLLISION_TILES - $6000) + sta $d + lda #0 ; Clear out the collision tile + ldy $2 + sta ($c),y sta Player_Y_Speed ;init player's vertical speed + jmp bank7_stab_brick_at_0E_with_A_and_does_draw ExitFireballNearby: rts ;leave! - +ExitBackToBump: + pla + pla + jmp SolidOrClimb ProcFireball_Bubble: ; lda PlayerStatus ;check player's status ; cmp #$02 diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index d82b26c9f..a636f5437 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -127,6 +127,7 @@ Left_Dir = %00000010 Right_Dir = %00000001 ;sound effects constants +;sq1 sfx Sfx_SmallJump = %10000000 Sfx_TailWag = %01000000 Sfx_Fireball = %00100000 @@ -136,6 +137,7 @@ Sfx_EnemyStomp = %00000100 Sfx_Bump = %00000010 Sfx_BigJump = %00000001 +;sq2 sfx Sfx_BowserFall = %10000000 Sfx_ExtraLife = %01000000 Sfx_PowerUpGrab = %00100000 @@ -144,6 +146,8 @@ Sfx_Blast = %00001000 Sfx_GrowVine = %00000100 Sfx_GrowPowerUp = %00000010 Sfx_CoinGrab = %00000001 +; noise sfx +Sfx_BrickShatter = %00000001 ;music constants Silence = %10000000 From 023d561051b37e8739aa68198f3d585669880529 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Tue, 14 Jul 2026 23:41:01 -0400 Subject: [PATCH 103/128] Replace flute song for warp whistle --- RandomizerCore/Asm/Flute.s | 15 +++++++++++++++ RandomizerCore/Asm/z2mario/integration.s | 7 ------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/RandomizerCore/Asm/Flute.s b/RandomizerCore/Asm/Flute.s index 6d90bf714..d9081b554 100644 --- a/RandomizerCore/Asm/Flute.s +++ b/RandomizerCore/Asm/Flute.s @@ -330,3 +330,18 @@ FREE_UNTIL $836f .proc TwisterSpawnPRG2 TwisterSpawnBody .endproc + +; patch the flute SFX to play the warp whistle tune +.segment "PRG6" +; note durations. the note lasts for the difference between two entries. +; So the first note lasts for $7f - $70 == $0f frames. These are read from +; right to left. +; when the next value >= current value then it doesn't process any more notes +.org $9644 +.byte $24, $24, $2C, $34, $60, $70, $7F +; list of pitch index. The pitches in the table aren't in a particular order, +; so look around in it or just replace unused ones if you want to fix the tuning :p +.byte $30, $3c, $4a, $4c, $4e, $30, $2c +; volume table (in the lower nybble). Make the sound fade a little less than vanilla +.org $9670 +.byte $51, $51, $52, $52, $53, $55 diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index c93d0336d..6a899068a 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -246,13 +246,6 @@ PatchGoingDownChimney: .org $e07f jmp *+3 -; Patch the flute sound to use the mario 3 music instead -; -- not happening right now. SMB3 flute is MUSIC instead of sfx, and i aint porting -; that right now. Music data is bank 28 at $a990 (M12ASegData03 in the disassembly) -; .segment "PRG0" -; .org $851d -; lda #Sfx_EnemySmack -; sta Square1SoundQueue .segment "PRG7" From 93b1a47cdd18a72a25afe63c60c293897c03697d Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 15 Jul 2026 10:55:31 -0400 Subject: [PATCH 104/128] WIP new texts for mario hints --- RandomizerCore/CustomTexts.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 2d562d666..9e8d55543 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -600,7 +600,7 @@ public static List GenerateTexts( } } - if (props.HelpfulHints) + if (props.HelpfulHints || props.MarioMode) { List placedIndexes = GenerateHelpfulHints(texts, itemLocations, hashRNG, props); GenerateKnowNothings(texts, placedIndexes, nonhashRNG, props.BagusWoods, props.UseCommunityText); @@ -730,6 +730,10 @@ private static Text GenerateWizardText(List texts, Random r, Location loca return new Text("HERE IS$HOW TO$PIERCE$SHIELDS."); case Collectable.UPSTAB: return new Text("UP B TO$THROW$HAMMER$WHEN SMALL"); + case Collectable.FIRE_SPELL: + return new Text("THIS MAGIC$WILL MAKE$YOUR HAND$THROW FIRE"); + case Collectable.SPELL_SPELL: + return new Text("SAY THE$MAGIC WORD$AND FEEL$THE POWER"); default: break; } From 6ac2a69573cff7ffb88bccacd273ee79ac319017 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 17 Jul 2026 00:54:44 -0400 Subject: [PATCH 105/128] Add tanooki statue and spell spell star power --- RandomizerCore/Asm/ram.inc | 21 +- RandomizerCore/Asm/z2mario/integration.s | 323 +++++++++++++++++++---- RandomizerCore/Asm/z2mario/map.s | 1 + RandomizerCore/Asm/z2mario/mario.s | 91 +++++-- RandomizerCore/Asm/z2mario/metasprite.s | 29 +- RandomizerCore/Asm/z2mario/sfx.s | 87 +++++- RandomizerCore/Asm/z2r.inc | 2 +- 7 files changed, 471 insertions(+), 83 deletions(-) diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index 3f0ac3db3..7011ddb59 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -217,6 +217,7 @@ Enemy3Type = $a4 Enemy4Type = $a5 Enemy5Type = $a6 ; Enemy (Boss) slot LinkCollisionBits = $a7 ; 0000ABLR (above, below, left, right) +EnemyState = $a8 Enemy0State = $a8 Enemy1State = $a9 Enemy2State = $aa @@ -599,9 +600,9 @@ RESV JumpSwimTimer RESV RunningTimer RESV SideCollisionTimer ; RESV BlockBounceTimer -FRAME_TIMER_COUNT = SideCollisionTimer - Timers -; RESV StarInvincibleTimer -ALL_TIMER_COUNT = SideCollisionTimer - Timers ; StarInvincibleTimer - Timers +RESV StarInvincibleTimer +FRAME_TIMER_COUNT = StarInvincibleTimer - Timers +ALL_TIMER_COUNT = StarInvincibleTimer - Timers RESV SpriteShuffleOffset ;RESV CurrentCHRBank @@ -627,15 +628,19 @@ RESV Noise_SfxLenCounter RESV TailWag_CtlHold RESV TailWag_FreqLo RESV TailWag_FreqHi +; have to keep this separate from the tail wag since they can proc at the same time +RESV Poof_CtlHold RESV JustThrownHammer RESV TailSwingTimer -; SMB3 raccoon flutter: nonzero = Y-velocity cap active RESV TailWagCount -; Per-enemy "hit by hammer" flag. Persistent across the enemy's hit-flash so -; later per-enemy recoil setters ($9AF9 etc., which run after ProjectileProcessing -; is cleared) can still know to suppress the knockback. Cleared when the flash -; timer at $040E,x decrements to zero (see bank7_Display patch). +; Not in the standard timer place so we can tick it down manually +RESV StatueTimer +; duration for specifically the poof effect timer +RESV StatueChangeTimer +; We need to surpress enemy knockback when they are hit by a hammer +; but the place we process the knockback is way later, so this stores "hits" +; on a per-enemy basis so that we can avoid knockback. RESV HammerHitFlag, 6 ; Shadow Mario Boss RAM diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 6a899068a..97d0c8983 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -2,7 +2,7 @@ .include "z2r.inc" .import GameRoutines, ProcFireball_Bubble, PlayerGfxHandler, DrawMetasprite -.import ProcHammerTime +.import ProcHammerTime, ProcStarPower .import Square1SfxHandler, Square2SfxHandler, NoiseSfxHandler .import SwapToSavedPRG, SwapToPRG0 .export SlightlyModifiedCollisionRoutine @@ -13,6 +13,12 @@ FREE "PRG0" [$92BF, $962D) ; Clear out the old flame / sword projectile code FREE "PRG0" [$9815, $9924) +STATUE_DURATION = $C0 +STATUE_CHANGE_DURATION = $17 +STATUE_BOUNCE_FORCE = $60 +STATUE_FALL_RESTORE = $70 +STAR_DURATION = 180 ; 3 seconds arbitrarily chosen because i like it + .segment "PRG7" SpellCastingRoutine = $8DC3 ; Link Main .org $D3EC ; patches the main sideview routine right before checking marios code @@ -163,8 +169,17 @@ ApplySpellEffectButPreventBothFireAndJump: rts @notFire: cpy #1 ; jump/tanooki spell bit? - bne @apply + bne @notJump and #~$10 ; clear the fire bit + rts +@notJump: + ; casting SPELL spell, so lets give the player a little star power time. + cpy #6 + bne @apply + pha + lda #STAR_DURATION + sta StarInvincibleTimer + pla @apply: rts @@ -187,12 +202,17 @@ DrawFallingMarioSprite: .org $8fee jsr DrawFallingMarioSprite +.segment "PRG7" + ; patch the link draw routine to skip drawing him with the vanilla code .org $EC02 ; ldx $11 lda $29,x jsr PatchLinkDrawRoutine - nop +; due to weird design decisions i made early on, the above patch double returns +; and it probably doesnt need to but whatever. + ; this guts drawing all the different link animation frames +FREE_UNTIL $ecea ; skip over the hitbox loading code for downstab +FREE "PRG7" [$ED02,$EE51) -.segment "PRG7" .org $dd1b jsr SetMarioShadowFlag @@ -267,11 +287,12 @@ CheckIfAboveScreenForFallingFairyCheck: .segment "PRG0", "PRG7" .reloc PatchLinkDrawRoutine: - ldx $11 - beq + ; link is slot 0 - lda $29,x - rts -+ +; TODO: check if this is still needed for dark link or mario's shadow after killing a boss +; ldx $11 +; beq + ; link is slot 0 +; lda $29,x +; rts +; + ; put the player in slot 4 always lda #4 sta CurrentOAMOffset @@ -283,10 +304,18 @@ PatchLinkDrawRoutine: pha jsr ApplySwingOverrides ldx ObjectMetasprite ; reload after override + ; Replace the metasprite we loaded in X with the poof animation frame + lda StatueChangeTimer + beq @noPoof + jsr GetStatuePoofFrame + @noPoof: ldy #0 jsr DrawMetasprite lda $D0 ; nonzero if Tanooki is active beq + + ; if we are poofing, hide both mario and the tail sprite + lda StatueChangeTimer + bne + jsr DrawTailSprite + pla ; restore PlayerFacingDir @@ -341,6 +370,108 @@ PatchLinkDrawRoutine: pla rts +.reloc +.import METASPRITE_POOF_FRAME_1, METASPRITE_POOF_FRAME_2, METASPRITE_POOF_FRAME_3, METASPRITE_POOF_FRAME_4 +StatuePoofFrames: +; the poof animation timer counts DOWN so the frames are not like i expected them to be :p + .byte METASPRITE_POOF_FRAME_2, METASPRITE_POOF_FRAME_1 + .byte METASPRITE_POOF_FRAME_4, METASPRITE_POOF_FRAME_3 +.reloc +; SMB3 cycles four poof patterns, one every four frames, its not perfect, but its good enough... +GetStatuePoofFrame: + lda StatueChangeTimer + lsr + lsr + and #$03 + tay + ldx StatuePoofFrames,y + rts + +.reloc +; Check for the right input to start a statue and then carry +.proc ProcTanookiStatue + ; The change poof runs on its own clock: it overlaps the statue when entering + ; and outlives it when leaving, so it can't just hang off StatueTimer. + lda StatueChangeTimer + beq @noPoof + dec StatueChangeTimer + bne @noPoof + dec ScrollLock ; balances the inc in StartStatueChange +@noPoof: + + lda StatueTimer + bne @active + + ; skip statue check if we can statue + lda JumpSpellActive + beq @runSwing + lda GameEngineSubroutine + cmp #8 + bne @runSwing + lda FairyState + bne @runSwing + + ; check our inputs to see if its time to start statueing + lda SavedJoypadBits + and #Down_Dir + beq @runSwing + lda A_B_Buttons + and #B_Button + beq @runSwing + and PreviousA_B_Buttons + bne @runSwing + ; start the statue! kill our x momentum and swap colors + lda #STATUE_DURATION + sta StatueTimer + jsr StartStatueChange + lda #0 + sta Player_X_Speed + sta Player_X_MoveForce + sta TailSwingTimer + sta TailWagCount + jmp UpdatePlayerPalette +@runSwing: + jmp ProcTanookiSwing + +@active: + ; continue statue + dec StatueTimer + cmp #1 + beq @end + ; check if the player wants to cancel it + lda SavedJoypadBits + and #Down_Dir + beq @end + ; or if they manage to cast fairy while in a statue, cancel it + lda FairyState + bne @end + ; not cancelling it so just return + rts +@end: + lda #0 + sta StatueTimer + lda #STATUE_FALL_RESTORE + sta VerticalForceDown + jsr StartStatueChange + jmp UpdatePlayerPalette +.endproc + +.reloc +; SMB3 plays the poof sfx on both edges of the statue change. +StartStatueChange: + ; prevent the screen from scroll away because we suddenly stopped moving + ; and lock mario in place during the POOF + lda StatueChangeTimer + bne @alreadyLocked + inc ScrollLock +@alreadyLocked: + lda #STATUE_CHANGE_DURATION + sta StatueChangeTimer + lda Square2SoundQueue + ora #Sfx_StatuePoof + sta Square2SoundQueue + rts + .reloc .proc ProcTanookiSwing @@ -465,9 +596,10 @@ Xhi = R5 Ylo = R6 Yhi = R7 - ; If a swing is in progress, the tail is forced to TAIL_STRAIGHT for the - ; entire animation regardless of Mario's body pose. + ; Force the metasprite for the tail if we are either swinging the tail or + ; are a statue lda TailSwingTimer + ora StatueTimer beq @noSwing ldx #METASPRITE_TAIL_STRAIGHT jmp @havePose @@ -550,7 +682,13 @@ Yhi = R7 @flipRight: lda #0 sta Atr - + + ; If we are stone mario, bring in the stone palette from mario + lda Player_SprAttrib + and #$03 + ora Atr + sta Atr + ; if stuck in mud/swamp copy over the render behind flag lda $0752 and #$20 @@ -680,14 +818,20 @@ UpdatePlayerPalette: inc ReloadCHRBank ; Just reload the CHR bank to be safe here txa pha - ; Check fire state and update the palette if we are fire still ldx #255 + ; The statue's gray wins over fire + lda StatueTimer + beq @notStatue + ldx #14-1 + bne @writeCommand ; unconditionall +@notStatue: + ; Check fire state and update the palette if we are fire still lda $76f and #$10 - beq + + beq @writeCommand ldx #7-1 - + - ; we are still firey so write a palette update for next NMI +@writeCommand: + ; write a palette update for next NMI ldy BUFFER_OFF dey - @@ -707,10 +851,13 @@ UpdatePlayerPalette: pla tax rts +.reloc PlayerRegularPalettePPUCommand: .byte $3f, $11, $03, $16, $27, $18, $ff PlayerFirePalettePPUCommand: .byte $3f, $11, $03, $37, $27, $16, $ff +PlayerStatuePalettePPUCommand: + .byte $3f, $11, $03, $00, $10, $2D, $ff .segment "PRG0", "PRG7" ; Don't clear fire state when transitioning to side views @@ -752,6 +899,82 @@ SetupMarioControl: .segment "PRG7" +; Hook the link hit routine to check if we are doing star power. If we are +; then we want to ZAP ZAP them with a thunder attack. +.org $E2F1 + jsr PatchLinkHitDispatch ; replaces lda $0518 +.reloc +; X = enemy slot, $10 = slot +PatchLinkHitDispatch: + lda StarInvincibleTimer + beq @notStar + lda EnemyState,x + and #~$10 + sta EnemyState,x + ; The custom boss has its own invincibility window that 50 damage a frame + ; would blow straight through, so star only blocks its hits. + lda boss_animation + bne @skiphit + ; check if the enemy is currently already hit by us + lda Enemy0HitState,x + bne @skiphit + ; copy of the code at $9215 which is where thunder damages enemies + ldy EnemyType,x + ; check the attributes for the monster id + lda $6E41,y + ; index for the damage value. 9 is the default thunder strike for 50 damage + ldy #9 + ; bit 7 is thunder immune (ie: they don't die to star either which is fine by me) + and #$A0 + bmi @skiphit + beq @damage + ; thunder bird has an extra bit 6 to say that we should use a weak hit instead + ldy #$01 +@damage: + ; double return since we didn't get hit + pla + pla + ; Run the deal damage routine with the "projectile attack" flag ($0B) set, + ; otherwise a surviving enemy recoils mario like a melee sword hit would. + ; $0B doubles as mario's Up_Down_Buttons, so save and restore it. + lda $0B + pha + lda #1 + sta $0B + jsr $E726 + pla + sta $0B + rts +@notStar: + lda StatueTimer + ora StatueChangeTimer + beq @normal + lda EnemyState,x + and #~$10 + sta EnemyState,x +@skiphit: + clc + ; double return since we didn't get hit winky face + pla + pla + rts +@normal: + jmp CheckLinkInvulnerable + +; Patch where the game checks links immunity frames, and add in all of the rest of the new ones +; that way we can fake being immune with all our other timers in one go. +.org $E457 + jsr CheckLinkInvulnerable +.reloc +CheckLinkInvulnerable: + ; slam all of the immunity timers together if any are running, then we are immune + lda $0518 + ora StarInvincibleTimer + ora StatueTimer + ora StatueChangeTimer +@done: + rts + ; Has to be in fixed bank .org $e337 jsr LinkTookDamage @@ -909,10 +1132,12 @@ NoDecTimers: ; This has to come before the A/B buttons are switched over jsr ProcHammerTime jsr ProcFireball_Bubble ;process fireballs and air bubbles - jsr ProcTanookiSwing + jsr ProcStarPower + jsr ProcTanookiStatue - lda A_B_Buttons ;save current A and B button - sta PreviousA_B_Buttons ;into temp variable to be used on next frame + lda SavedJoypadBits ;save the real A and B buttons for next frame. + and #%11000000 ;A_B_Buttons is zeroed while statued, so using it here + sta PreviousA_B_Buttons ;made B look freshly pressed when the statue ended lda #$00 sta Left_Right_Buttons ;nullify left and right buttons temp variable @@ -1279,16 +1504,6 @@ SlightlyModifiedCollisionRoutine: jsr $E079 ; skip ahead to near the end of the collision check jmp SwapToPRG0 -; .org $E073 - ; jsr PatchCollisionCheck - - -; .segment "PRG7" -; .reloc -; PatchCollisionCheck: - ; lda PlayerStandingMetatile - ; sta $03 ; will be checked to see what metatile you stepped on - ; jmp $E095 ; in collision but after the metatile collider ; Recenter mario's bg collision hitbox when checking what position he is at. ; This changes where marios "center" metatile to match better with his visuals @@ -1299,26 +1514,28 @@ SlightlyModifiedCollisionRoutine: .org $ee7b .byte $40, $40 -; In vanilla, when link goes off screen to the left, it will go to page $ff, so the -; game has code that bumps the player up a page to counter that offset -; .org $e177 ; inc Player_PageLoc - ; nop - ; nop - .segment "PRG0", "PRG7" .org $92A5 jmp PatchFinishPlayerPaletteCycle .reloc PatchFinishPlayerPaletteCycle: - sta $0301 ; write player flash counter + sta $0301 ldy $074B ; flash frame counter ; If its the last frame of the spell cycle bne + - lda $69DE ; and we aren't using shield spell - cmp #$02 - beq + - ; then update our colors - stx $0301 ; reset the position of the vram buffer write so we don't double write colors - jmp UpdatePlayerPalette ; And then update the player palette appropriately + ; reset the position of the vram buffer write so we don't double write colors + stx $0301 + jmp UpdatePlayerPalette + + + rts + +.org $9241 + jmp PatchFlashDecorEnd +.reloc +PatchFlashDecorEnd: + sta $074B + bcc + + ; carry is set when the cycle ends so bring back in our original palette + jmp UpdatePlayerPalette + rts @@ -1457,9 +1674,7 @@ SkipRecoilForStabs: .org $E65E jsr CheckForDownstab bcc $E66A - LDA #$fc - STA $057D - rts + jmp SetStompBounce .assert * <= $E66A ; Disable recoil when hitting enemy with hammers @@ -1517,6 +1732,7 @@ FREE_UNTIL $E708 SetDownstabStatTracking: ;set recoil to -4 to bounce up sta $057D + jsr ApplyStatueBounceForce ; stomped an enemy so inc stats as well inc StatDownStabCount+0 bne @Exit @@ -1538,6 +1754,23 @@ InyIfDownstabbing: + rts +.reloc +; Stomp physics between SMB1 and SMB3 are quite different, so add extra bounce force +; to make it feel better. i just like it mmmkay +ApplyStatueBounceForce: + lda StatueTimer + beq @done + lda #STATUE_BOUNCE_FORCE + sta VerticalForceDown +@done: + rts + +.reloc +SetStompBounce: + lda #$fc + sta Player_Y_Speed + jmp ApplyStatueBounceForce + .reloc CheckForDownstab: ; i broke this out into its own function cause originally it was much longer diff --git a/RandomizerCore/Asm/z2mario/map.s b/RandomizerCore/Asm/z2mario/map.s index 2c25c4426..a312066e4 100644 --- a/RandomizerCore/Asm/z2mario/map.s +++ b/RandomizerCore/Asm/z2mario/map.s @@ -22,6 +22,7 @@ .org $8728 jmp DrawMarioMap +FREE_UNTIL $87A2 .reloc DrawMarioMap: sec diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 595d375ea..1ad1de36a 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -2,19 +2,21 @@ .include "z2r.inc" .import SlightlyModifiedCollisionRoutine -.import PlayerBankTable .import METASPRITE_SMALL_MARIO_DEATH, FIRE_MARIO_OFFSET, METASPRITE_FIRE_MARIO_SWIMMING_STILL_1, METASPRITE_SMALL_FIRE_SWIMMING_STILL_1 .import METASPRITE_BIG_MARIO_JUMPING, METASPRITE_BIG_MARIO_SWIMMING_1_KICK, METASPRITE_BIG_MARIO_STANDING, METASPRITE_BIG_MARIO_SKIDDING -.import METASPRITE_BIG_MARIO_WALKING_1, METASPRITE_BIG_MARIO_CLIMBING_1, METASPRITE_BIG_MARIO_CROUCHING, METASPRITE_FIRE_MARIO_STANDING +.import METASPRITE_BIG_MARIO_WALKING_1, METASPRITE_BIG_MARIO_CROUCHING, METASPRITE_FIRE_MARIO_STANDING .import METASPRITE_SMALL_MARIO_JUMPING, METASPRITE_SMALL_MARIO_SWIMMING_1_KICK, METASPRITE_SMALL_MARIO_STANDING, METASPRITE_SMALL_MARIO_SKIDDING -.import METASPRITE_SMALL_MARIO_WALKING_1, METASPRITE_SMALL_MARIO_CLIMBING_1, METASPRITE_SMALL_MARIO_GROW_STANDING, METASPRITE_FIREBALL_FRAME_1 +.import METASPRITE_SMALL_MARIO_WALKING_1, METASPRITE_SMALL_MARIO_GROW_STANDING, METASPRITE_FIREBALL_FRAME_1 .import METASPRITE_FIREBALL_FRAME_2, METASPRITE_EXPLOSION_FRAME_1, METASPRITE_EXPLOSION_FRAME_2, METASPRITE_EXPLOSION_FRAME_3 .import METASPRITE_HAMMER_FRAME_1, METASPRITE_HAMMER_FRAME_2 .import SWIMMING_ANIMATION_FRAME_COUNT -.export GameRoutines, ProcFireball_Bubble, PlayerGfxHandler +.export GameRoutines, ProcFireball_Bubble, PlayerGfxHandler, ProcStarPower .segment "PRG0", "PRG7" +STATUE_FLICKER_START = $60 ; warning flicker once this many frames remain +STAR_WARNING_FRAMES = 32 ; palette cycle slows down below this + .reloc ;------------------------------------------------------------------------------------- @@ -197,6 +199,18 @@ SaveJoyp: and #%00001100 sta Up_Down_Buttons + ; If we are in a statue pose, then clear the player input to prevent any other + ; mario actions from happening. + lda StatueTimer + ora StatueChangeTimer + beq @notStatue + lda #$00 + sta A_B_Buttons + sta Left_Right_Buttons + sta Up_Down_Buttons +@notStatue: + + lda Up_Down_Buttons and #%00000100 ;check for pressing down beq SizeChk ;if not, branch lda Player_State ;check player's state @@ -207,10 +221,14 @@ SaveJoyp: sta Left_Right_Buttons ;if pressing down while on the ground, sta Up_Down_Buttons ;nullify directional bits SizeChk: - lda $a7 ; flip collision bits to match mario style + lda LinkCollisionBits ; flip collision bits to match mario style eor #$ff - sta $a7 - jsr PlayerMovementSubs ;run movement subroutines + sta LinkCollisionBits + ; force mario to freeze while changing + lda StatueChangeTimer + bne @frozen + jsr PlayerMovementSubs ;run movement subroutines +@frozen: ldy #$01 ;is player small? lda PlayerSize bne ChkMoveDir @@ -238,9 +256,9 @@ PlayerSubs: ; jsr BoundingBoxCore ;get player's bounding box coordinates jsr PlayerBGCollision ;do collision detection and process - lda $a7 ; flip collision bits to match z2 style + lda LinkCollisionBits ; flip collision bits to match z2 style eor #$ff - sta $a7 + sta LinkCollisionBits ; This is the offset for the table that holds the Y add val for link ldy #$1d @@ -377,7 +395,18 @@ CntPl: cmp #$0b ;branch ahead to some other part bne + jmp PlayerKilled -+ lda PlayerChangeSizeFlag ;if grow/shrink flag set ++ lda StatueTimer + beq @notInStatue + cmp #STATUE_FLICKER_START + bcs @statueDraw ;plenty of time left, so draw solid + and #$02 ;SMB3 warning: 2 frames drawn, 2 frames hidden + bne ClearMarioSprite + @statueDraw: + lda #METASPRITE_BIG_MARIO_STANDING + sta ObjectMetasprite + jmp PlayerOffscreenChk ;keep the scroll position fresh, skip anim/swim +@notInStatue: + lda PlayerChangeSizeFlag ;if grow/shrink flag set beq + jmp DoChangeSize ;then branch to some other code + @@ -489,7 +518,7 @@ PlayerGfxTblOffsets: .byte METASPRITE_BIG_MARIO_STANDING .byte METASPRITE_BIG_MARIO_SKIDDING .byte METASPRITE_BIG_MARIO_WALKING_1 - .byte METASPRITE_BIG_MARIO_CLIMBING_1 + .byte $00 ; METASPRITE_BIG_MARIO_CLIMBING_1 .byte METASPRITE_BIG_MARIO_CROUCHING .byte METASPRITE_FIRE_MARIO_STANDING ; .byte $80, $88, $b8, $78, $60, $a0, $b0, $b8 @@ -498,7 +527,7 @@ PlayerGfxTblOffsets: .byte METASPRITE_SMALL_MARIO_STANDING .byte METASPRITE_SMALL_MARIO_SKIDDING .byte METASPRITE_SMALL_MARIO_WALKING_1 - .byte METASPRITE_SMALL_MARIO_CLIMBING_1 + .byte $00 ; METASPRITE_SMALL_MARIO_CLIMBING_1 .byte METASPRITE_SMALL_MARIO_DEATH GrowAnimation = * - PlayerGfxTblOffsets .byte METASPRITE_SMALL_MARIO_GROW_STANDING @@ -646,6 +675,28 @@ ResetPalStar: sta Player_SprAttrib ;store as new player attributes rts ;and leave +.reloc +; The SPELL spell's star power. The metasprite engine ORs Player_SprAttrib into +; every player tile, so we just do the same style palette cycle mario 1 does +.proc ProcStarPower + lda StarInvincibleTimer + bne @active + lda Player_SprAttrib + and #%11111100 + sta Player_SprAttrib + rts +@active: + cmp #STAR_WARNING_FRAMES + lda FrameCounter + bcs @fast + ; every 8 frames + lsr + lsr +@fast: + lsr ; every other frame + jmp CyclePlayerPalette +.endproc + ;------------------------------------------------------------------------------------- .reloc PlayerMovementSubs: @@ -1190,8 +1241,8 @@ OffscrJoypadBitsData: .reloc ProcessPlayerAction: lda Player_State ;get player's state - cmp #$03 - beq ActionClimbing ;if climbing, branch here + ; cmp #$03 + ; beq ActionClimbing ;if climbing, branch here cmp #$02 beq ActionFalling ;if falling, branch here cmp #$01 @@ -1250,12 +1301,12 @@ ActionWalkRun: jsr GetGfxOffsetAdder ;get offset to graphics table jmp FourFrameExtent ;execute instructions for normal state -ActionClimbing: - ldy #$05 ;load offset for climbing - lda Player_Y_Speed ;check player's vertical speed - beq NonAnimatedActs ;if no speed, branch, use offset as-is - jsr GetGfxOffsetAdder ;otherwise get offset for graphics table - jmp ThreeFrameExtent ;then skip ahead to more code +; ActionClimbing: +; ldy #$05 ;load offset for climbing +; lda Player_Y_Speed ;check player's vertical speed +; beq NonAnimatedActs ;if no speed, branch, use offset as-is +; jsr GetGfxOffsetAdder ;otherwise get offset for graphics table +; jmp ThreeFrameExtent ;then skip ahead to more code ActionSwimming: ldy #$01 ;load offset for swimming diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index 2fe75d7bd..076df770a 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -656,10 +656,10 @@ MetaspriteBox "FIRE_MARIO", "SWIMMING_1_HOLD", $00, $02, $24, $22 MetaspriteBox "FIRE_MARIO", "SWIMMING_2_HOLD", $04, $06, $24, $26 MetaspriteBox "FIRE_MARIO", "SWIMMING_3_HOLD", $04, $06, $28, $22 -BIG_MARIO_CLIMBING_1_BANK = CHR_MARIOACTION -BIG_MARIO_CLIMBING_2_BANK = CHR_MARIOACTION -MetaspriteBox "BIG_MARIO", "CLIMBING_1", $00, $02, $08, $0a -MetaspriteBox "BIG_MARIO", "CLIMBING_2", $04, $06, $0c, $0e +; BIG_MARIO_CLIMBING_1_BANK = CHR_MARIOACTION +; BIG_MARIO_CLIMBING_2_BANK = CHR_MARIOACTION +; MetaspriteBox "BIG_MARIO", "CLIMBING_1", $00, $02, $08, $0a +; MetaspriteBox "BIG_MARIO", "CLIMBING_2", $04, $06, $0c, $0e SMALL_MARIO_VRAM_OFFSET = SPRITE_BANK_0 SMALL_MARIO_BANK = CHR_SMALLMARIO @@ -701,8 +701,8 @@ MetaspriteBox "SMALL_FIRE", "SWIMMING_1_HOLD", $00, $02, $18, $1a MetaspriteBox "SMALL_FIRE", "SWIMMING_2_HOLD", $00, $02, $2c, $2e MetaspriteDuplicate "SMALL_FIRE_SWIMMING_3_HOLD", "SMALL_FIRE_SWIMMING_3_KICK" -MetaspriteBox "SMALL_MARIO", "CLIMBING_1", $18, $1a -MetaspriteBox "SMALL_MARIO", "CLIMBING_2", $38, $3a +; MetaspriteBox "SMALL_MARIO", "CLIMBING_1", $18, $1a +; MetaspriteBox "SMALL_MARIO", "CLIMBING_2", $38, $3a MetaspriteBox "SMALL_MARIO", "DEATH", $34, $36 SMALL_MARIO_GROW_STANDING_BANK = CHR_SMALLMARIO @@ -745,6 +745,23 @@ MetaspriteBox "EXPLOSION", "FRAME_1", $00, $00 | SPR_FLIP_H MetaspriteBox "EXPLOSION", "FRAME_2", $02, $02 | SPR_FLIP_H MetaspriteBox "EXPLOSION", "FRAME_3", $04, $04 | SPR_FLIP_H +; Tiles used for the "poof" effect +POOF_FRAME_1_VRAM_OFFSET = SPRITE_BANK_2 +POOF_FRAME_2_VRAM_OFFSET = SPRITE_BANK_2 +POOF_FRAME_3_VRAM_OFFSET = SPRITE_BANK_1 +POOF_FRAME_4_VRAM_OFFSET = SPRITE_BANK_1 +POOF_PALETTE = $01 +POOF_X_OFFSET = 8 +POOF_FRAME_3_X_OFFSET = 12 +POOF_FRAME_4_X_OFFSET = 12 +POOF_FRAME_1_Y_OFFSET = 2 +POOF_FRAME_2_Y_OFFSET = 2 +POOF_FRAME_3_Y_OFFSET = -4 +POOF_FRAME_4_Y_OFFSET = -4 +MetaspriteBox "POOF", "FRAME_1", $08, $08 | SPR_FLIP_H +MetaspriteBox "POOF", "FRAME_2", $06, $06 | SPR_FLIP_H +MetaspriteBox "POOF", "FRAME_3", $38 +MetaspriteBox "POOF", "FRAME_4", $3a HAMMER_FRAME_1_VRAM_OFFSET = SPRITE_BANK_2 HAMMER_FRAME_2_VRAM_OFFSET = SPRITE_BANK_0 diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s index 2aea62abb..40906fdf0 100644 --- a/RandomizerCore/Asm/z2mario/sfx.s +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -225,6 +225,76 @@ Square1_Table_Notes: .word $1AB8, $1938, $17CC, $1678, $1534, $1404 .word $12E4, $11D4, $10D4, $0FE0, $0EFC, $0E24 +PlayStatuePoof: + lda #SndLev1_DataPoof - SndLev1_DataLongWag + sta Squ2_SfxLenCounter + lda #$9F + sta Poof_CtlHold + ; fall through into ContinueStatuePoof for the first frame + +ContinueStatuePoof: + inc Squ2_SfxLenCounter +@fetch: + ldy Squ2_SfxLenCounter + lda SndLev1_DataLongWag-1,y + beq @streamEnd + bpl @playNote + sta Poof_CtlHold + inc Squ2_SfxLenCounter + bne @fetch + +@playNote: + tay + lda Poof_CtlHold + sta SND_SQUARE2_REG + lda #$7F + sta SND_SQUARE2_REG+1 + tya + jmp Poof_PokeNote + +@streamEnd: + lda #$00 + sta SND_SQUARE2_REG+2 + sta Squ2_SfxLenCounter + sta Square2SoundBuffer + rts + +; Square 2 twin of TailWag_PokeNote (SMB3 PRG31 $E7C1) +Poof_PokeNote: + cmp #$7E + beq @rest + pha + ldx #$01 +@octLoop: + inx + sec + sbc #24 + bpl @octLoop + clc + adc #24 + tay + lda Square1_Table_Notes,y + sta TailWag_FreqLo + lda Square1_Table_Notes+1,y + sta TailWag_FreqHi +@octShift: + lsr TailWag_FreqHi + ror TailWag_FreqLo + dex + bne @octShift + pla + cmp #56 + bcc @noAdjust + dec TailWag_FreqLo +@noAdjust: + lda TailWag_FreqLo + sta SND_SQUARE2_REG+2 + lda TailWag_FreqHi + ora #%00001000 + sta SND_SQUARE2_REG+3 +@rest: + rts + PlaySmallJump: lda #$26 ;branch here for small mario jumping sound bne JumpRegContents @@ -488,7 +558,9 @@ Square2SfxHandler: ldy Square2SoundQueue ;check for sfx in queue beq CheckSfx2Buffer sty Square2SoundBuffer ;if found, put in buffer and check for the following - bmi PlayBowserFall ;bowser fall + bpl + + jmp PlayStatuePoof ;tanooki statue change (was bowser fall) + + lsr Square2SoundQueue bcs PlayCoinGrab ;coin grab lsr Square2SoundQueue @@ -509,8 +581,9 @@ Square2SfxHandler: CheckSfx2Buffer: lda Square2SoundBuffer ;check for sfx in buffer beq ExS2H ;if not found, exit sub - bmi ContinueBowserFall ;bowser fall - lsr + bpl + + jmp ContinueStatuePoof ;tanooki statue change (was bowser fall) + + lsr bcs Cont_CGrab_TTick ;coin grab lsr bcs ContinueGrowItems ;power-up reveal @@ -701,3 +774,11 @@ SMB3FluteSong: .byte $18, $88, $7E, $7E, $A8, $3A, $84, $7E, $88, $3A, $A4, $30, $36, $38, $A8, $3A ; $AD19 - $AD28 .byte $84, $7E, $88, $3A, $A4, $30, $36, $38, $94, $04, $02, $00, $94, $0A, $0B, $07 ; $AD29 - $AD38 .byte $07, $00 + +; SMB3 SndLev1_DataPoof from $A57B +SndLev1_DataPoof: + .byte $9F + .byte $2E, $2A, $26, $22, $9D, $2E, $2A, $7E, $7E, $9F, $30, $2E, $2A, $28, $9D, $30 ; $A57B - $A58A + .byte $2E, $7E, $7E, $9F, $38, $34, $32, $30, $9D, $38, $34, $32, $30, $9A, $38, $34 ; $A58B - $A59A + .byte $32, $30, $9C, $38, $34, $32, $30, $97, $38, $34, $32, $30, $98, $38, $34, $32 ; $A59B - $A5AA + .byte $30, $94, $38, $34, $32, $30, $00 diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index a636f5437..8a51247cb 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -138,7 +138,7 @@ Sfx_Bump = %00000010 Sfx_BigJump = %00000001 ;sq2 sfx -Sfx_BowserFall = %10000000 +Sfx_StatuePoof = %10000000 Sfx_ExtraLife = %01000000 Sfx_PowerUpGrab = %00100000 Sfx_TimerTick = %00010000 From 1b05573baec2042b3e177a6f427c2b12f2c547ba Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 17 Jul 2026 14:24:39 -0400 Subject: [PATCH 106/128] Allow flute warps to different locations --- CrossPlatformUI/Lang/Resources.resx | 8 +- CrossPlatformUI/Views/Tabs/SpellsView.axaml | 10 +- RandomizerCore/Asm/Flute.s | 214 ++++++++++++++------ RandomizerCore/Asm/StatTracking.s | 19 ++ RandomizerCore/Asm/ram.inc | 4 +- RandomizerCore/EnumTypes.cs | 14 ++ RandomizerCore/Hyrule.cs | 6 +- RandomizerCore/ROM.cs | 40 +++- RandomizerCore/RandomizerConfiguration.cs | 4 +- RandomizerCore/RandomizerProperties.cs | 2 +- 10 files changed, 246 insertions(+), 75 deletions(-) diff --git a/CrossPlatformUI/Lang/Resources.resx b/CrossPlatformUI/Lang/Resources.resx index 450f54a65..1b6f700a7 100644 --- a/CrossPlatformUI/Lang/Resources.resx +++ b/CrossPlatformUI/Lang/Resources.resx @@ -858,7 +858,13 @@ dash, the Dash spell will still appear, but do nothing. Link will always shoot beams from his sword, regardless of life. -Flute works like the Zelda 1 flute and spawns a twister you can walk into to warp the player to dungeons that they've already cleared (placed a gem in). If you are facing up or right, then it will take you to the next dungeon in order, if you are facing down or left, it will take you to the previous dungeon in order. +Flute works like the Zelda 1 flute and spawns a twister you can walk into to warp the player. +Facing up or right takes you to the next destination in order; facing down or left takes you to the previous one. +Available Modes: + • No Flute Warp - disables the feature. + • Cleared Palaces - warps to palaces you've already cleared (placed a gem in). + • Visited Palaces - warps to any palace you've entered, including the Great Palace. + • Visited Towns - warps to any town you've entered. When enabled, your starting items will be randomized. diff --git a/CrossPlatformUI/Views/Tabs/SpellsView.axaml b/CrossPlatformUI/Views/Tabs/SpellsView.axaml index bd2e132ee..347ea54f6 100644 --- a/CrossPlatformUI/Views/Tabs/SpellsView.axaml +++ b/CrossPlatformUI/Views/Tabs/SpellsView.axaml @@ -80,12 +80,14 @@ > - - + diff --git a/RandomizerCore/Asm/Flute.s b/RandomizerCore/Asm/Flute.s index d9081b554..3977357e2 100644 --- a/RandomizerCore/Asm/Flute.s +++ b/RandomizerCore/Asm/Flute.s @@ -4,6 +4,13 @@ TWISTER_ENEMY_ID = $04 TWISTER_TILE = $b5 ; tile IDs for frame 0; frame 1 uses TWISTER_TILE+2 TWISTER_PAL = $02 ; OAM palette attribute (bits 0-1) +; Exactly one of the following is defined by ROM.FluteTwisterWarp() to select the mode: +; FLUTE_WARP_CLEARED_PALACES - warp to palaces whose crystal has been placed +; FLUTE_WARP_VISITED_PALACES - warp to any palace entered (incl. Great Palace) +; FLUTE_WARP_VISITED_TOWNS - warp to any town entered +; The "visited" modes track destinations in the FluteWarpFlags bitfield +; and if none of them are defined, then this file isn't getting built at all. + ; Overworld "Area Byte 0" (Y position + flags) table in work RAM, indexed by ; LocationNumber. Bit 7 set marks the slot as a palace/town entrance; when it's ; clear the sideview loader treats the slot as a generic/random-battle area. @@ -11,10 +18,23 @@ OverworldAreaTable = $6a00 ; UpdateHiddenPalaceSpot() stores the flute-hidden palace's *revealed* Area Data ; The palace is index 0 at $df68 and the town is index 1 at $df69 HiddenPalaceRevealedAreaUpdate = $df68 +.ifdef FLUTE_WARP_VISITED_TOWNS +; In towns mode the hidden destination is a town (New Kasuto), so reveal $df69 instead +; TODO: test what happens if a town is behind 3 eye rock +HiddenRevealAddr = HiddenPalaceRevealedAreaUpdate + 1 +.else +HiddenRevealAddr = HiddenPalaceRevealedAreaUpdate +.endif + +; Weirdly the game doesn't have a power of two table except in bank 0 and 3 +.segment "PRG7" +.reloc +PowersOfTwo: + .byte $01, $02, $04, $08, $10, $20, $40, $80 .segment "PRG0", "PRG7" -.import PalaceMappingTable +.import RealPalaceNumberTable ProcessNextOverworldEnemy = $842F UpdateOverworldSpritePosition = $8397 @@ -51,7 +71,7 @@ OverworldPostLoadFluteCheck: sta $0726 rts @enter: - sta LocationNumber ; restore the palace area index ($34 + palace code) + sta LocationNumber ; restore the destination area index sta AreaEntranceIndex lda #$00 sta TwisterPendingPalaceLocation @@ -59,12 +79,12 @@ OverworldPostLoadFluteCheck: lda #5 sta GameMode ; the destination continent's area byte table was just reloaded from ROM, so - ; reveal the hidden palace now (if that's where we're headed) -RevealHiddenPalaceIfNeeded: + ; reveal the hidden destination now (if that's where we're headed) +RevealHiddenDestIfNeeded: ldy LocationNumber lda OverworldAreaTable, y bmi @done - lda HiddenPalaceRevealedAreaUpdate + lda HiddenRevealAddr sta OverworldAreaTable, y @done: rts @@ -77,7 +97,7 @@ TwisterDrawCheck: lda OverworldEnemy0Type, x cmp #$04 beq @twister - tax ; do the original + tax ; do the original rts @twister: ; type 4: write twister tile and palette @@ -96,7 +116,6 @@ TwisterDrawCheck: pla pla jmp UpdateOverworldSpritePosition - .reloc TwisterVelocityCheck: @@ -133,6 +152,22 @@ TwisterWarp: lda #$00 sta OverworldEnemy0Type, x sta OverworldDemonTimer, x +.ifdef FLUTE_WARP_VISITED_TOWNS + ; TwisterCurrentPalaceSlot holds a town code (0-7). Look up the overworld area-table + ; index of that town's primary entrance from TownWarpLocationTable which is built + ; and exported by the C# randomizer code. + ldy TwisterCurrentPalaceSlot + lda TownWarpLocationTable, y + sta LocationNumber + sta AreaEntranceIndex + ; target region: codes 0-3 = West (region 0), 4-7 = East (region 2) + tya + and #$04 + beq @have_region + lda #$02 +@have_region: +.else + ; TwisterCurrentPalaceSlot holds a global palace slot (0-15). ; LocationNumber = $34 + within-region palace code lda TwisterCurrentPalaceSlot and #$03 @@ -144,6 +179,7 @@ TwisterWarp: lda TwisterCurrentPalaceSlot lsr lsr +.endif cmp RegionNumber ; already in the right continent so we don't need to switch continents beq @same_region @@ -162,12 +198,12 @@ TwisterWarp: lda #$ff sta GameMode ; the area byte table isn't loaded for the destination continent yet, so the - ; hidden palace reveal is deferred to OverworldPostLoadFluteCheck + ; hidden reveal is deferred to OverworldPostLoadFluteCheck bne @load ; unconditional @same_region: ; still in the destination continent, so its area byte table is already live. - ; reveal the palace destination if we are warping there - jsr RevealHiddenPalaceIfNeeded + ; reveal the hidden destination if we are warping there + jsr RevealHiddenDestIfNeeded @load: pla pla @@ -181,16 +217,16 @@ TwisterWarp: lda OverworldEnemy0Type, x cmp #$04 ; already have a twister so skip processing it - bne @skip + bne @skip_active jmp @exit -@skip: +@skip_active: dex bpl @active_check - ; Check the player direction and find a valid palace to warp to + ; Check the player direction to decide which way to walk the destination list. ; Direction: OverworldFacingDirection bits ; next $01=right $08=up - ; previous $02=left $04=down + ; previous $02=left $04=down lda OverworldFacingDirection ; right ($01) or up ($08) and #$09 @@ -198,52 +234,71 @@ TwisterWarp: lda #$ff ; add -1 to walk the list backwards bne @scan ; unconditional @go_next: - lda #$01 ; add 1 to walk the slot list forwards + lda #$01 ; add 1 to walk the list forwards @scan: sta R7 - ; skip through the list of crystals placed to find the next palace to go to ldy TwisterCurrentPalaceSlot - lda PalaceMappingTable,y - jsr @ConvertPalaceIdToMapping - ldx #5 -@next_palace: +.ifdef FLUTE_WARP_VISITED_TOWNS + ; Visited towns: walk town codes 0-7. + ; TwisterCurrentPalaceSlot is already the town slot + tya +.else + ; Palace modes: walk palace numbers 0-5/6, then map back to a global slot + lda RealPalaceNumberTable,y +.endif + and #7 + sta R6 + ldx #7 + +@next_dst: + lda R6 clc adc R7 and #7 - cmp #6 ; skip over the great palace - bcs @next_palace + sta R6 + ; Make sure that we don't warp to something unavailable in the slots + ; when using a palace warp not all bits are used in the FluteWarpFlags +.ifndef FLUTE_WARP_VISITED_TOWNS +.ifdef FLUTE_WARP_CLEARED_PALACES + cmp #6 ; skip gp (#6 since theres no crystal) and the unused value 7 +.else ; FLUTE_WARP_VISITED_PALACES + cmp #7 ; only skip the unused value 7 +.endif + bcs @skip_dst +.endif ; .ifndef FLUTE_WARP_VISITED_TOWNS tay - lda CrystalPlaced, y - bne @crystalplaced - tya + lda FluteWarpFlags + and PowersOfTwo,y + bne @found_dst dex - bpl @next_palace - ; We haven't placed any crystals, so leave + bpl @next_dst + ; nothing visited yet, so leave jmp @exit -@crystalplaced: - sty R7 ; use this as a temp storage for the target palace to warp to - ; store the next palace idx that we will warp to - ; now that we know what palace we want to go to, loop through the palace mapping table to find the - ; actual location for it. +@found_dst: +.ifdef FLUTE_WARP_VISITED_TOWNS + ; town code is used directly as the slot + lda R6 + sta TwisterCurrentPalaceSlot +.else + ; R6 = target palace number. Loop the mapping table to find its global slot. ldy #$0f @next_mapping_entry: - lda PalaceMappingTable, y - cmp #$ff ; $ff is a place holder indicating no palace at this slot in the world + lda RealPalaceNumberTable, y + cmp #$ff ; $ff means no palace at this slot beq @advance - jsr @ConvertPalaceIdToMapping - ; Check if this palace number == the actual palace we are looking for - cmp R7 - beq @foundPalaceToWarpTo - ; haven't found the palace mapping table entry for this palace yet, so loop again + cmp R6 + beq @foundSlot @advance: dey bpl @next_mapping_entry - ; we couldn't find the mapping? that doesn't seem possible... + ; we couldn't find the mapping? that shouldn't be possible... jmp @exit -@foundPalaceToWarpTo: +@foundSlot: sty TwisterCurrentPalaceSlot +.endif +@spawn: ; Find a free encounter slot ldx #$07 @free_slot: @@ -274,31 +329,64 @@ TwisterWarp: ; And also check if its time to do spider/3 eye rock encounter lda OverworldFacingTerrain rts -@ConvertPalaceIdToMapping: - ; This table layout was optimized for updating stats (since thats done every frame) - ; and this doesn't matter how long it takes pretty much. So we need to convert - sec - sbc #Palace1Offset - ; div by 3 to get the palace number 0 - 6 - sta R5 - lsr - lsr - adc R5 - ror - lsr - adc R5 - ror - lsr - adc R5 - ror - lsr - adc R5 - ror - lsr - rts .endmacro +; If our flute mode needs to save visited locations, then include the following code +.if .defined(FLUTE_WARP_VISITED_PALACES) .or .defined(FLUTE_WARP_VISITED_TOWNS) +.segment "PRG7" + +.ifdef FLUTE_WARP_VISITED_TOWNS +.reloc +; Maps from vanilla town ID to the "actual" town present at this spot. +TownWarpLocationTable: + .byte RealTownAtLocation0, RealTownAtLocation1, RealTownAtLocation2, RealTownAtLocation3 + .byte RealTownAtLocation4, RealTownAtLocation5, RealTownAtLocation6, RealTownAtLocation7 +.endif + +.org $CBB4 + jsr FluteMarkVisited + +.reloc +FluteMarkVisited: + sta GameMode ; the store we replaced (A == 0 here) +.ifdef FLUTE_WARP_VISITED_TOWNS + ; 1 = West town, 2 = East town, >=3 = palace + lda WorldNumber + cmp #1 + beq @town + cmp #2 + bne @done +@town: + ; already region-adjusted to 0-7 + ldy TownNumber + lda PowersOfTwo,y + ora FluteWarpFlags + sta FluteWarpFlags +@done: + rts +.else ; FLUTE_WARP_VISITED_PALACES + lda WorldNumber + cmp #3 + bcc @done ; < 3 means we entered a town/cave, not a palace + lda RegionNumber + asl + asl + adc PalaceRegionIndex + tay + lda RealPalaceNumberTable,y + cmp #7 ; guard against an unexpected empty slot + bcs @done + tay + lda PowersOfTwo,y + ora FluteWarpFlags + sta FluteWarpFlags +@done: + rts +.endif +.endif + + .segment "PRG1" ; Skip east continent check diff --git a/RandomizerCore/Asm/StatTracking.s b/RandomizerCore/Asm/StatTracking.s index e75a2e7a5..acefe1610 100644 --- a/RandomizerCore/Asm/StatTracking.s +++ b/RandomizerCore/Asm/StatTracking.s @@ -138,6 +138,19 @@ PalaceMappingTable: .byte RealPalaceAtLocation4 * 3 + Palace1Offset .byte $ff, $ff, $ff ; 3 unused palace locations +.reloc +; Same as the above table but not optimized for stat tracking +.export RealPalaceNumberTable +RealPalaceNumberTable: + ; region 0 - east hyrule + .byte RealPalaceAtLocation1, RealPalaceAtLocation2, RealPalaceAtLocation3, $ff + ; region 1 - death mountain (no palaces) + .byte $ff, $ff, $ff, $ff + ; region 2 - west hyrule + .byte RealPalaceAtLocation5, RealPalaceAtLocation6, RealPalaceAtLocationGP, $ff + ; region 3 - maze island + .byte RealPalaceAtLocation4, $ff, $ff, $ff + .segment "PRG4" @@ -152,6 +165,12 @@ SaveTimestampForPalace: asl adc PalaceRegionIndex tay +.ifdef FLUTE_WARP_CLEARED_PALACES +.import PowersOfTwo + lda FluteWarpFlags + ora PowersOfTwo,y + sta FluteWarpFlags +.endif lda PalaceTable,y jmp AddTimestamp diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index 7011ddb59..33fcf53cf 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -442,7 +442,7 @@ HaveFlute = $0789 HaveCross = $078a HaveHammer = $078b HaveMagicKey = $078c -CrystalPlaced = $078d ; base of crystal-placed flags, indexed by palace_code (0-5); non-zero = gem placed +CrystalPlaced = $078d ; crystal-placed flags, unused in randomizer Keys = $0793 ; Current number of Keys Crystals = $0794 ; Number of Crystals left HaveStabs = $0796 ; 0x04 = Have Downstab | 0x10 = Have Upstab @@ -452,6 +452,8 @@ HaveBagusNoteAndMedicine = $079a ; 0x08 = Bagu's Note | 0x40 = Medicine HaveWater = $079b ; 1 = Have Water HaveChild = $079c ; 0x20 = Have Child HaveBasementAccess = $079d ; 0x08 = Have New Kasuto Basement access +; Bitlist of palaces cleared/towns visited/etc for flute warp mode +FluteWarpFlags = $079e ContinuesUsed = $079f ; These are new variables that use free memory diff --git a/RandomizerCore/EnumTypes.cs b/RandomizerCore/EnumTypes.cs index 3365a9e56..fa691df54 100644 --- a/RandomizerCore/EnumTypes.cs +++ b/RandomizerCore/EnumTypes.cs @@ -193,6 +193,19 @@ public enum FireOption RANDOM } +[DefaultValue(NONE)] +public enum FluteWarpMode +{ + [Description("No Flute Warp")] + NONE, + [Description("Cleared Palaces")] + CLEARED_PALACES, + [Description("Visited Palaces")] + VISITED_PALACES, + [Description("Visited Towns")] + VISITED_TOWNS, +} + [DefaultValue(VANILLA)] public enum PalaceStyle @@ -927,6 +940,7 @@ public static class Enums public static IEnumerable EnemyLifeOptionList { get; } = ToDescriptions(); public static IEnumerable BossLifeOptionList { get; } = ToDescriptions(i => i != EnemyLifeOption.WIDE); public static IEnumerable FireOptionList { get; } = ToDescriptions(); + public static IEnumerable FluteWarpModeList { get; } = ToDescriptions(); public static IEnumerable BossRoomMinDistanceOptions { get; } = ToDescriptions(); public static IEnumerable PalaceLengthOptionList { get; } = ToDescriptions(); public static IEnumerable PalaceItemRoomCountOptions { get; } = ToDescriptions(); diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index ac8e89721..f6ec8e5c7 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3770,6 +3770,8 @@ public void StatTracking(RandomizerProperties props, Assembler asm) a.Set("_ALLOW_ITEM_DUPLICATES", props.AllowImportantItemDuplicates ? 1 : 0); if (props.MarioMode) a.Assign("ENABLE_Z2_MARIO", 1); + if (props.FluteWarpMode == FluteWarpMode.CLEARED_PALACES) + a.Assign("FLUTE_WARP_CLEARED_PALACES", 1); a.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.StatTracking.s"), "stat_tracking.s"); } @@ -3938,9 +3940,9 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando rom.DashSpell(engine); } - if (props.FluteTwisterWarp) + if (props.FluteWarpMode != FluteWarpMode.NONE) { - ROM.FluteTwisterWarp(engine); + ROM.FluteTwisterWarp(engine, props.FluteWarpMode); } if (props.UpARestartsAtPalaces) diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 835c0864f..2f5e52f13 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -2168,12 +2168,50 @@ public void AggressiveThunderbird() Put(0x16413, thunderBirdHP); } - public static void FluteTwisterWarp(Assembler asm) + public static void FluteTwisterWarp(Assembler asm, FluteWarpMode mode) { var a = asm.Module(); + // Exactly one mode define is set so Flute.s only assembles that mode's code paths. + switch (mode) + { + case FluteWarpMode.CLEARED_PALACES: + a.Assign("FLUTE_WARP_CLEARED_PALACES", 1); + break; + case FluteWarpMode.VISITED_PALACES: + a.Assign("FLUTE_WARP_VISITED_PALACES", 1); + break; + case FluteWarpMode.VISITED_TOWNS: + a.Assign("FLUTE_WARP_VISITED_TOWNS", 1); + AssignTownWarpLocations(a); + break; + case FluteWarpMode.NONE: + default: + throw new ArgumentOutOfRangeException(nameof(mode), mode, "NONE must not assemble Flute.s"); + } a.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.Flute.s"), "flute.s"); } + // Similar to the real palace location table, this is a map of town ID to the real town ID + private static void AssignTownWarpLocations(AsmModule a) + { + LocationID[] townWarpLocations = + [ + LocationID.WEST_TOWN_RAURO, + LocationID.WEST_TOWN_RUTO, + LocationID.WEST_TOWN_SARIA_NORTH, + LocationID.WEST_TOWN_MIDO, + LocationID.EAST_TOWN_NABOORU, + LocationID.EAST_TOWN_DARUNIA, + LocationID.EAST_TOWN_NEW_KASUTO, + LocationID.EAST_TOWN_OLD_KASUTO, + ]; + for (int i = 0; i < townWarpLocations.Length; i++) + { + var (_, locIdx) = townWarpLocations[i].GetIndices(); + a.Assign($"RealTownAtLocation{i}", locIdx); + } + } + public void DashSpell(Assembler asm) { var a = asm.Module(); diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 228a80668..faa268054 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -685,7 +685,7 @@ private bool palaceStylesAnyMetastyleSelected() private bool marioMode; [Reactive] - private bool fluteTwisterWarp; + private FluteWarpMode fluteWarpMode; //Custom [Reactive] @@ -1465,7 +1465,7 @@ public RandomizerProperties Export(Random r, bool includeDifficulty = true) }; properties.JumpAlwaysOn = jumpAlwaysOn; properties.DashAlwaysOn = dashAlwaysOn; - properties.FluteTwisterWarp = fluteTwisterWarp; + properties.FluteWarpMode = fluteWarpMode; properties.FastCast = fastSpellCasting; properties.BeamSprite = beamSprite; properties.DisableMusic = disableMusic; diff --git a/RandomizerCore/RandomizerProperties.cs b/RandomizerCore/RandomizerProperties.cs index 8a19ca006..3e93f3e51 100644 --- a/RandomizerCore/RandomizerProperties.cs +++ b/RandomizerCore/RandomizerProperties.cs @@ -219,7 +219,7 @@ public class RandomizerProperties public byte BeepThreshold { get; set; } public bool JumpAlwaysOn { get; set; } public bool DashAlwaysOn { get; set; } - public bool FluteTwisterWarp { get; set; } + public FluteWarpMode FluteWarpMode { get; set; } public bool FastCast { get; set; } public BeamSprites BeamSprite { get; set; } public bool DisableMusic { get; set; } From 0e356f16496048fad3b3e9e356a9dd896b9ab304 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Fri, 17 Jul 2026 23:41:55 -0400 Subject: [PATCH 107/128] Add a new title screen sequence for z2 mario. Just the bare bones skeleton for now. Replaces the stars with a new star animation handler and frees up some space in the bank. --- RandomizerCore/Asm/MMC5.s | 26 +- RandomizerCore/Asm/ram.inc | 6 + .../Asm/z2mario/CustomTitleScreen.s | 381 ++++++++++++++++++ RandomizerCore/Asm/z2mario/boss.s | 20 + RandomizerCore/Hyrule.cs | 12 +- RandomizerCore/ROM.cs | 9 +- RandomizerCore/RandomizerCore.csproj | 2 + 7 files changed, 448 insertions(+), 8 deletions(-) create mode 100644 RandomizerCore/Asm/z2mario/CustomTitleScreen.s diff --git a/RandomizerCore/Asm/MMC5.s b/RandomizerCore/Asm/MMC5.s index e95ab1cf5..f84d8b515 100644 --- a/RandomizerCore/Asm/MMC5.s +++ b/RandomizerCore/Asm/MMC5.s @@ -23,7 +23,7 @@ FREE "PRG7" [$FEDE, $FFDC) .org $fffa .word Nmi .org $fffe - .word IrqHdlr + .word IRQTrampoline bank7_code0 = $c000 bank7_Remove_All_Sprites = $d24c @@ -106,7 +106,6 @@ SetupScanlineIRQ: .proc IrqHdlr pha lda LineIrqStatusReg - lda PpuCtrlForIrq sta PPUCTRL sta PpuCtrlShadow ; Not sure about this @@ -119,6 +118,24 @@ SetupScanlineIRQ: rti .endproc ; IrqHdlr +; We have two different IRQ routines, one for the X only split, and one for the Y only split +; when loading into a save file, setup the X only split for gameplay. +.segment "PRG5" +.org $b2b6 + jsr LoadMainIRQRoutineHandler +.reloc +LoadMainIRQRoutineHandler: + sta $0772 + ; write JMP IrqHdlr to RAM + lda #$4c + sta IRQTrampoline + lda #IrqHdlr + sta IRQTrampolineAddrHi + rts + +.segment "PRG7" ; Summary of the bug ; On a lag frame NMI is skipped, which means the hud scroll value of 0 isn't set ; We can work around this by never disabling NMI and using a soft disable instead @@ -612,6 +629,8 @@ ClearStackRAM: ;;;;; Fix sword flashing on title screen bug ; Switch the OAM position of an unused sprite and the glitchy sword tile ; and move that just off to the left. This sprite just doesn't seem to glitch? +; We have an entire custom title screen in z2mario so just ignore this whole thing +.ifndef ENABLE_Z2_MARIO .org $A7C1 + 22 * 4 .byte $80,$E8,$20,$70 @@ -628,6 +647,8 @@ ClearStackRAM: and #$23 sta $0267 + 4,x +.endif ; ENABLE_Z2_MARIO + .segment "PRG7" ; Update the pointers to the bank switches @@ -824,7 +845,6 @@ LoadAreaBGMetatile: ; switch back to 0 just in case jmp SwapToPRG0 - ; Move sprites out of the last fixed bank ; Instead of using the blank tile sprites to cover the right 8px on the overworld, just use ; any other full color sprite (since the palette is all black this is fine) diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index 33fcf53cf..a727d21d3 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -474,6 +474,12 @@ SavedDoorExit2ScrollX = $69b2 ; index. z2ft needs this, at this specific address. AreaEntranceIndex = $69ff +; We need a horizontal IRQ for gameplay and a vertical IRQ for the title screen +; so we manage to keep both by making a small `jmp Irq` in sram +IRQTrampoline = $7ffd +IRQTrampolineAddrLo = $7ffe +IRQTrampolineAddrHi = $7fff + ; All Timestamp types listed below for reference TS_COUNT .set 0 .macro TimestampId name diff --git a/RandomizerCore/Asm/z2mario/CustomTitleScreen.s b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s new file mode 100644 index 000000000..8a3009ce3 --- /dev/null +++ b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s @@ -0,0 +1,381 @@ +.include "z2r.inc" + +;; Custom title screen for z2 mario +; Removes the sword sprite +; adds a pipe and mario animation +; makes the scroll split use irq instead +; redo all the star sprites to use a new custom twinkle code + +.segment "PRG5" + +NUM_SKY_STARS = 25 +NUM_WATER_STARS = 11 +NUM_STARS = NUM_SKY_STARS + NUM_WATER_STARS + +STAR_TILE_C3 = $e8 +STAR_TILE_C2 = $ea +STAR_TILE_C1 = $ec +SHOOTING_STAR_TILE = STAR_TILE_C2 + +seed = $6690 +StarColorIndex = $6691 +StarTimer = $66B5 +StarBaseTimer = $66D9 +ShootingStarY = $66FD +ShootingStarX = $66FE +ShootingStarActive = $66FF +TitleTempIndex = $6700 + +TitleIrqActive = $6701 + +; Copies of the scroll data that will be written during IRQ this frame +TitleIrqPpuCtrl = $6702 +TitleIrqNametableHi = $6703 +TitleIrqNametableLo = $6704 + + +TITLE_SPLIT_IRQ_LINE = 142 + +FREE "PRG5" [$A7C1, $A932) + +.org $A72E + jsr InitTitleStars + +.org $AE41 + jsr InitTitleStars + +.org $A6F7 + jmp *+6 + +; Replace the title screen sprite zero with IRQ. With that, we are free from sprite zero! ... +; ... only in mario mode lol + +; Sprite/OAM work runs after setting the split IRQ so that timing-critical IRQ +; doesn't get delayed due to running the mario code +.org $A737 + jsr SetTitleSplitIrq + jsr UpdateTitleSprites + jmp $A76A +FREE_UNTIL $A76A + +.org $AB6D + jsr SetTitleSplitIrq + jsr UpdateTitleSprites + jmp $ABA0 +FREE_UNTIL $ABA0 + +.reloc +SetTitleSplitIrq: + lda PpuCtrlShadow + and #$FC + ora $36 + sta TitleIrqPpuCtrl + lda $27 + sta TitleIrqNametableHi + lda $28 + sta TitleIrqNametableLo + + ; write JMP IrqHdlr to RAM + lda #$4c + sta IRQTrampoline + lda #TitleSplitIrqBody + sta IRQTrampolineAddrHi + lda #TITLE_SPLIT_IRQ_LINE + sta LineIrqTgtReg + lda #1 + sta TitleIrqActive + lda #ENABLE_SCANLINE_IRQ + sta LineIrqStatusReg + cli + rts + +.reloc +TitleSplitIrqBody: + pha + ; burn some cycles to hide the scroll split + pha + pla + pha + pla + pha + pla + lda TitleIrqPpuCtrl + sta PpuCtrlShadow + sta PPUCTRL + lda TitleIrqNametableHi + sta PPUADDR + lda TitleIrqNametableLo + sta PPUADDR + lda PPUDATA + lda PPUDATA + lda PPUSTATUS + lda #0 + sta TitleIrqActive + sta LineIrqStatusReg + pla + rti + + +; New palette for the stars +.org $AF51 + .byte $10, $30, $21 + + +.reloc +RockOamTable: + .byte $7F, $F0, $F8 + .byte $3F, $EE, $F8 + .byte $4F, $F6, $F0 + .byte $4F, $F4, $F8 + .byte $5F, $EE, $E8 + .byte $5F, $F0, $F0 + .byte $5F, $F0, $F8 + .byte $6F, $EE, $D0 + .byte $6F, $F0, $D8 + .byte $6F, $F4, $E0 + .byte $6F, $F2, $E8 + .byte $6F, $F6, $F0 + .byte $6F, $F0, $F8 + .byte $7F, $F0, $D0 + .byte $7F, $F2, $D8 + .byte $7F, $F4, $E0 + .byte $7F, $F0, $E8 + .byte $7F, $F2, $F0 +ROCK_TABLE_LEN = (* - RockOamTable) + +StarPosTableY: + .byte $07, $07, $0F, $0F, $5F, $17, $1F, $1F, $5F, $1F, $4F, $1F, $1F + .byte $27, $27, $27, $2F, $2F, $37, $37, $3F, $3F, $47, $47, $4F ; sky (25) + .byte $AF, $B4, $9F, $AD, $92, $95, $8F, $95, $98, $78, $93 ; water (11) + +StarPosTableX: + .byte $50, $A0, $20, $78, $40, $E0, $10, $28, $60, $A0, $80, $E0, $E8 + .byte $78, $C8, $E8, $28, $D8, $10, $C8, $A8, $18, $08, $48, $E0 ; sky (25) + .byte $40, $20, $58, $48, $70, $88, $90, $B0, $8A, $95, $8E ; water (11) + +TileForColor: + .byte STAR_TILE_C1, STAR_TILE_C2, STAR_TILE_C3 + +.reloc +BarebonesTitleScreenRNG: + lda seed + asl a + bcs @NoEor + eor #$46 +@NoEor: + adc #$EB + sta seed + rts + +; Initialize the star timers and colors. The old stars/water sparkles worked by cycling through +; different palette indicies, so they would use each of the colors from color 3 in the different palettes +; Well thats a problem when i want to use mario on the screen, and stars shouldn't twinkle brown. +; So instead, we lose one color from the star twinkle, (the deep $02 blue from before), and instead +; put all stars on the same palette index 0. We then use two different "star sprites" that use color +; 1 and 2 as well as the vanilla color 3. That way the stars can twinkle independently of each other. +.reloc +InitTitleStars: + lda FrameCounter + sta seed + + ldx #0 +@SkyLoop: + jsr BarebonesTitleScreenRNG + and #7 + clc + adc #1 + asl + asl + asl + sta StarBaseTimer,x + sta StarTimer,x + jsr BarebonesTitleScreenRNG + and #3 + sta StarColorIndex,x + inx + cpx #NUM_SKY_STARS + bne @SkyLoop + +@WaterLoop: + jsr BarebonesTitleScreenRNG + and #7 + clc + adc #1 + asl + asl + sta StarBaseTimer,x + sta StarTimer,x + jsr BarebonesTitleScreenRNG + and #3 + sta StarColorIndex,x + inx + cpx #NUM_STARS + bne @WaterLoop + + ; Shooting star: starting position copied from vanilla entry 27. + lda #$07 + sta ShootingStarY + lda #$28 + sta ShootingStarX + lda #0 + sta ShootingStarActive + sta TitleIrqActive + rts + +.reloc +UpdateTitleSprites: + lda #0 + sta CurrentOAMOffset + + jsr DrawTitleOverlay + jsr DrawTitlePipe + jsr DrawTitleMario + jsr DrawTitleRocks + jsr DrawTitleStars + + ; Hide whatever OAM slots are left unclaimed this frame. + ldx CurrentOAMOffset + beq @Done +@HideRemaining: + lda #$F8 + sta Sprite_Y_Position,x + inx + inx + inx + inx + bne @HideRemaining +@Done: + rts + +.reloc +DrawTitleOverlay: + ; TODO: sprites that mask the pipe rising/lowering out of the ground. + rts + +.reloc +DrawTitlePipe: + ; TODO: the pipe itself. + rts + +.reloc +DrawTitleMario: + ; TODO: mario's title screen sprite + rts + +.reloc +DrawTitleRocks: + ldx CurrentOAMOffset + ldy #0 +@RockLoop: + lda RockOamTable,y + sta Sprite_Y_Position,x + lda RockOamTable+1,y + sta Sprite_Tilenumber,x + lda #1 + sta Sprite_Attributes,x + lda RockOamTable+2,y + sta Sprite_X_Position,x + inx + inx + inx + inx + iny + iny + iny + cpy #ROCK_TABLE_LEN + bne @RockLoop + stx CurrentOAMOffset + rts + +.reloc +DrawTitleStars: + ldy CurrentOAMOffset + lda ShootingStarActive + bne @SSMove + lda FrameCounter + bne @SSDraw + lda #1 + sta ShootingStarActive + bne @SSDraw +@SSMove: + lda FrameCounter + and #3 + bne @SSCheckLimit + lda ShootingStarY + clc + adc #2 + sta ShootingStarY + lda ShootingStarX + clc + adc #3 + sta ShootingStarX +@SSCheckLimit: + lda ShootingStarY + cmp #$37 + bcc @SSDraw + lda #0 + sta ShootingStarActive + lda #$F8 + sta ShootingStarY +@SSDraw: + lda ShootingStarY + sta Sprite_Y_Position,y + lda #SHOOTING_STAR_TILE + sta Sprite_Tilenumber,y + lda #$20 ; background + sta Sprite_Attributes,y + lda ShootingStarX + sta Sprite_X_Position,y + iny + iny + iny + iny +@Done: + ldx #0 +@StarLoop: + dec StarTimer,x + bne @CheckColor + lda StarBaseTimer,x + sta StarTimer,x + inc StarColorIndex,x + lda StarColorIndex,x + and #3 + sta StarColorIndex,x +@CheckColor: + lda StarColorIndex,x + beq @NextStar + cpy #0 + beq @NextStar ; ran outta oam slots, but keep processing sprites + + stx TitleTempIndex + tax + lda TileForColor-1,x + pha + ldx TitleTempIndex + lda StarPosTableY,x + sta Sprite_Y_Position,y + pla + sta Sprite_Tilenumber,y + lda Sprite_Y_Position,y + cmp #TITLE_SPLIT_IRQ_LINE + bcs + + lda #$20 ; background for the stars + bne @writeAttrs + + + lda #0 ; foreground for the water + @writeAttrs: + sta Sprite_Attributes,y + lda StarPosTableX,x + sta Sprite_X_Position,y + iny + iny + iny + iny +@NextStar: + inx + cpx #NUM_STARS + bne @StarLoop + sty CurrentOAMOffset + rts diff --git a/RandomizerCore/Asm/z2mario/boss.s b/RandomizerCore/Asm/z2mario/boss.s index 13cb149c7..e9431f12e 100644 --- a/RandomizerCore/Asm/z2mario/boss.s +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -103,6 +103,26 @@ BossLandedInCutscene: jmp $9a77 FREE_UNTIL $9a0f +; Patch the cutscene movement state and run it ourselves +.org $9864 + jsr BossCutsceneFall + +; Free the "cutscene" movement routine +FREE "PRG5" [$9A0F, $9A77) +; Free the sword collision code +FREE "PRG5" [$9A85, $9B3A) + +.reloc +BossCutsceneFall: + ; Launch dark mario using the X movement/speed already set + ldx #0 + jsr $DEB8 ; Simple_Horizontal_Movement + lda #$3C + sta $00 + lda #$04 + sta $02 + jmp $DECE ; applyGravityMotion + ; Make dark mario use a different hitbox (vanilla has a jank $0e hitbox, switch ; to a simple tall enemy hitbox with $00) .org $9540 diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index f6ec8e5c7..71caeb969 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2464,6 +2464,16 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra var title_graphics = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.mario_title.chr"); rom.Put(ROM.ChrRomOffset + 0x1380, title_graphics[0..0x300]); rom.Put(ROM.ChrRomOffset + 0x1840, title_graphics[0x300..0x400]); + // I rewrote the star sparkle animation on the title screen to use different star sprites instead + // I build them from copying the existing star sprite + var starSpriteTile = rom.GetBytes(ROM.ChrRomOffset + 0x0e80, 0x20); + // Make a palette 1 and 2 version + var starSpriteTilePalette1 = starSpriteTile.ToArray(); + var starSpriteTilePalette2 = starSpriteTile.ToArray(); + starSpriteTilePalette1[3] = 0; + starSpriteTilePalette2[3+8] = 0; + rom.Put(ROM.ChrRomOffset + 0x0ea0, starSpriteTilePalette1); + rom.Put(ROM.ChrRomOffset + 0x0ec0, starSpriteTilePalette2); } if (props.EncounterRates == EncounterRate.NONE) @@ -3826,7 +3836,7 @@ .word NewCreditsBody private void MarioModeActivate(Assembler asm) { // "metasprite_engine.s" is included in metasprite.s - string[] modules = ["boss.s", "integration.s", "map.s", "mario.s", "metasprite.s", "sfx.s"]; + string[] modules = ["boss.s", "integration.s", "map.s", "mario.s", "metasprite.s", "sfx.s", "CustomTitleScreen.s"]; foreach (var mod in modules) { var a = asm.Module(); diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 2f5e52f13..f69250223 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -510,16 +510,17 @@ public void AddRandomizerToTitle(Assembler asm, bool marioMode, bool isRandomize ; SKIP THIS IF NOT RANDOMIZED .byte $22, $4a, 1 ; Write 1 useless byte to just skip this write .byte $f4 +; setup the attributes for the right hand side +.byte $23,$E0,REPEAT | 16,$00 +.byte $23,$E7,1,$cc +; .byte $23,$E8,REPEAT | 7,$00 +.byte $23,$EF,1,$cc Line1: ; ZELDA II .byte $22, $6C, 8 .byte $10, $11, $12, $13, $14, $15, $16, $17 ; setup the attributes for the right hand side -.byte $23,$E0,REPEAT | 16,$00 -.byte $23,$E7,1,$cc -; .byte $23,$E8,REPEAT | 7,$00 -.byte $23,$EF,1,$cc .byte $23,$f0,REPEAT | 16,$00 .byte $23,$f7,1,$cc ;.byte $23,$f8,REPEAT | 7,$00 diff --git a/RandomizerCore/RandomizerCore.csproj b/RandomizerCore/RandomizerCore.csproj index 66a9d0db9..95d4601c3 100644 --- a/RandomizerCore/RandomizerCore.csproj +++ b/RandomizerCore/RandomizerCore.csproj @@ -89,5 +89,7 @@ + + \ No newline at end of file From 4f1d7cda7b2d7646e07a7758efd98d7531aa2d79 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 18 Jul 2026 00:04:59 -0400 Subject: [PATCH 108/128] Fix scroll freeze by splitting out the mario one --- RandomizerCore/Asm/ram.inc | 5 +++++ RandomizerCore/Asm/z2mario/integration.s | 17 +++++++++-------- RandomizerCore/Asm/z2mario/mario.s | 11 ++++++----- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index a727d21d3..447699765 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -646,6 +646,11 @@ RESV TailWagCount RESV StatueTimer ; duration for specifically the poof effect timer RESV StatueChangeTimer +; Mario-only scroll freeze for shrinking/growing and tanooki statue. +; Originally I used the Z2 scroll freeze, but that meant that turning into a +; statue could cause a boss to start attacking since bosses start once the +; screen is locked lol +RESV MarioScrollFreeze ; We need to surpress enemy knockback when they are hit by a hammer ; but the place we process the knockback is way later, so this stores "hits" ; on a per-enemy basis so that we can avoid knockback. diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 97d0c8983..22d99a9f0 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -396,7 +396,7 @@ GetStatuePoofFrame: beq @noPoof dec StatueChangeTimer bne @noPoof - dec ScrollLock ; balances the inc in StartStatueChange + dec MarioScrollFreeze ; balances the inc in StartStatueChange @noPoof: lda StatueTimer @@ -459,11 +459,12 @@ GetStatuePoofFrame: .reloc ; SMB3 plays the poof sfx on both edges of the statue change. StartStatueChange: - ; prevent the screen from scroll away because we suddenly stopped moving - ; and lock mario in place during the POOF + ; prevent the screen from scrolling away because we suddenly stopped moving. + ; Use the mario-only freeze so we don't clamp mario into the arena bounds or + ; wake up a boss that is on-screen but not yet activated. lda StatueChangeTimer bne @alreadyLocked - inc ScrollLock + inc MarioScrollFreeze @alreadyLocked: lda #STATUE_CHANGE_DURATION sta StatueChangeTimer @@ -1008,8 +1009,8 @@ CheckIfTurningSmall: sta GameEngineSubroutine ; lda #8 ; sta InjuryTimer - ; we will unlock the scroll lock after the size change animation finishes - inc ScrollLock + ; we will unlock the scroll freeze after the size change animation finishes + inc MarioScrollFreeze lda #1 sta Player_State ; sta PlayerChangeSizeFlag @@ -1063,9 +1064,9 @@ CheckIfWeAreBig: ; Set player routine to ChangeSize lda #$0a sta GameEngineSubroutine - ; we will unlock the scroll lock after the size change animation finishes + ; we will unlock the scroll freeze after the size change animation finishes ; lda #1 - inc ScrollLock + inc MarioScrollFreeze lda #0 sta ScrollAmount sta Player_State diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 1ad1de36a..aa4605c91 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -585,17 +585,17 @@ PlayerChangeSize: EndChgSize: cmp #$c4 ;check again for another specific moment bne ExitChgSize ;and branch to leave if before or after that point - ; Z2Mario - clear scroll lock after you change size + ; Z2Mario - clear the mario scroll freeze after you change size ; lda #0 - dec ScrollLock + dec MarioScrollFreeze jmp DonePlayerTask ;otherwise do sub to init timer control and set routine ExitChgSize: rts ; TODO check this RTS can be removed ;and then leave FinishedInjuryBlink: - ; Z2Mario - clear scroll lock after you change size + ; Z2Mario - clear the mario scroll freeze after you change size ; lda #0 - dec ScrollLock + dec MarioScrollFreeze ; and also reset the injurytimer to extend your invincibility lda #$20 sta InjuryTimer @@ -1156,7 +1156,8 @@ ScrollHandler: ; clc ; adc Platform_X_Scroll ;add value used by left/right platforms ; sta Player_X_Scroll ;save as new value here to impose force on scroll - lda ScrollLock ;check scroll lock flag + lda ScrollLock ;check boss-arena scroll lock flag + ora MarioScrollFreeze ;...or the mario shrink/grow/tanooki freeze bne InitScrlAmt ;skip a bunch of code here if set lda Player_Pos_ForScroll cmp #$50 ;check player's horizontal screen position From 3337ded8d8d0d7979ce4332f27321b9cf323a411 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 18 Jul 2026 03:03:43 -0400 Subject: [PATCH 109/128] Controllable mario on the title screen Preparing for the rest of the animation! I really like controllable mario on the title screen though, so I will probably make it a easter egg / feature --- .../Asm/z2mario/CustomTitleScreen.s | 151 ++++++++++++++++-- RandomizerCore/Asm/z2mario/integration.s | 64 +++++--- 2 files changed, 176 insertions(+), 39 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/CustomTitleScreen.s b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s index 8a3009ce3..fc9756a9a 100644 --- a/RandomizerCore/Asm/z2mario/CustomTitleScreen.s +++ b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s @@ -1,5 +1,9 @@ .include "z2r.inc" +.import GameRoutines, PlayerGfxHandler, DrawMetasprite, PlayerBankTable +.import SwapToPRG0, SwapToSavedPRG +.import ClearExtraRAMOnScreenTransition, ClearCollisionRAM, BankSwitchMarioCHR + ;; Custom title screen for z2 mario ; Removes the sword sprite ; adds a pipe and mario animation @@ -36,6 +40,10 @@ TitleIrqNametableLo = $6704 TITLE_SPLIT_IRQ_LINE = 142 +; Spawn mario here for testing for now. +TITLE_MARIO_X = $78 +TITLE_MARIO_Y = $30 + FREE "PRG5" [$A7C1, $A932) .org $A72E @@ -94,13 +102,11 @@ SetTitleSplitIrq: .reloc TitleSplitIrqBody: pha - ; burn some cycles to hide the scroll split - pha - pla - pha - pla - pha - pla + ; burn some cycles to hide the scroll split - 29 cycles for 6 bytes + clc + lda #$2a + nop + bcc *-2 lda TitleIrqPpuCtrl sta PpuCtrlShadow sta PPUCTRL @@ -121,6 +127,24 @@ TitleSplitIrqBody: ; New palette for the stars .org $AF51 .byte $10, $30, $21 +.org $AF59 + .byte $16, $27, $18 +; The vanilla title screen uses Player_Y_Position ($29) to hold part of the scroll +; so relocate all of those to use $3a instead. +.org $AD09 + .byte $3A +.org $AD2B + .byte $3A +.org $AD2F + .byte $3A +.org $AD47 + .byte $3A +.org $AD53 + .byte $3A +.org $AD59 + .byte $3A +.org $ADE7 + .byte $3A .reloc @@ -132,13 +156,13 @@ RockOamTable: .byte $5F, $EE, $E8 .byte $5F, $F0, $F0 .byte $5F, $F0, $F8 - .byte $6F, $EE, $D0 + ; .byte $6F, $EE, $D0 .byte $6F, $F0, $D8 .byte $6F, $F4, $E0 .byte $6F, $F2, $E8 .byte $6F, $F6, $F0 .byte $6F, $F0, $F8 - .byte $7F, $F0, $D0 + ; .byte $7F, $F0, $D0 .byte $7F, $F2, $D8 .byte $7F, $F4, $E0 .byte $7F, $F0, $E8 @@ -214,7 +238,6 @@ InitTitleStars: cpx #NUM_STARS bne @WaterLoop - ; Shooting star: starting position copied from vanilla entry 27. lda #$07 sta ShootingStarY lda #$28 @@ -222,7 +245,87 @@ InitTitleStars: lda #0 sta ShootingStarActive sta TitleIrqActive - rts + + ; clear z2 collision ram + ldy #$d0 + lda #$11 ; value chosen to not match any "lava" or other weird tiles +@loop: + sta $6000-1,y + sta $6000-1+$d0,y + sta $6000-1+$d0*2,y + sta $6000-1+$d0*3,y + dey + bne @loop + + jmp SetupTitleMario + +.reloc +SetupTitleMario: + ; wipes timers, size/state flags, boss ram, etc + jsr ClearExtraRAMOnScreenTransition + jsr ClearCollisionRAM + + lda #8 ; PlayerCtrlRoutine index in the GameRoutines jump table + sta GameEngineSubroutine + lda #$28 ;store value here + sta VerticalForceDown ;for fractional movement downwards + ; Set our custom floor bytes + ldx #0 + stx Player_State + stx PlayerSize ; big mario + stx SwimmingFlag + stx CrouchingFlag + stx Player_X_Speed + stx Player_Y_Speed + stx Player_Y_MoveForce + stx LinkCollisionBits + inx + stx Player_Y_HighPos ; page 1 == onscreen + stx PlayerFacingDir + stx Player_MovingDir + stx Player_PageLoc + stx ScreenLeft_X_Pos + stx ScreenLeft_PageLoc + inx + stx Player_SprAttrib + + lda #TITLE_MARIO_X + sta Player_X_Position + lda #TITLE_MARIO_Y + sta Player_Y_Position + + ldx #8 +@collisionLoop: + sta $63d7,x + sta $63e7+$d0,x + sta $64b0+$d0,x + dex + bpl @collisionLoop + ; setup pipe collision + sta $63c7+$d0 + sta $63c8+$d0 + sta $63d7+$d0 + sta $63d8+$d0 + sta $64e2 ; tiny baby rock on the left + ldx #16 * 6 + @loop2: + ; do a left side rock as well. + sta $63c0+$d0,x + sta $63c1+$d0,x + ; and walls off the screen + sta $6377,x + sta $6457+$d0,x + txa + sec + sbc #16 + tax + bpl @loop2 + + + lda #CHR_BIGMARIO + sta PlayerChrBank + inc ReloadCHRBank + jmp BankSwitchMarioCHR .reloc UpdateTitleSprites: @@ -259,11 +362,33 @@ DrawTitlePipe: ; TODO: the pipe itself. rts +.segment "PRG7" .reloc DrawTitleMario: - ; TODO: mario's title screen sprite - rts + lda #5 + sta CurrentPrgBank + jsr SwapToPRG0 + .import DecrementMarioTimers, ProcessMarioInput + jsr DecrementMarioTimers + jsr GameRoutines + jsr PlayerGfxHandler + jsr ProcessMarioInput + + ldy ObjectMetasprite + lda PlayerBankTable,y + cmp PlayerChrBank + beq @SameBank + sta PlayerChrBank + inc ReloadCHRBank + jsr BankSwitchMarioCHR + @SameBank: + + ldx ObjectMetasprite + ldy #0 + jsr DrawMetasprite + jmp SwapToSavedPRG +.segment "PRG5" .reloc DrawTitleRocks: ldx CurrentOAMOffset diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 22d99a9f0..a066934a1 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -6,6 +6,7 @@ .import Square1SfxHandler, Square2SfxHandler, NoiseSfxHandler .import SwapToSavedPRG, SwapToPRG0 .export SlightlyModifiedCollisionRoutine +.export BankSwitchMarioCHR, ClearExtraRAMOnScreenTransition, ClearCollisionRAM ; Remove unused code after we gutted link's movement FREE "PRG0" [$92BF, $962D) @@ -1105,27 +1106,7 @@ PatchLinkMain: ; lda $93b5,y ; rts ; @notmoving: - ; decrement timers here cause lazy - ; Move the timers ahead by a frame as well - lda TimerControl ;if master timer control not set, decrement - beq DecTimers ;all frame and interval timers - dec TimerControl - bne NoDecTimers - DecTimers: - ldx #FRAME_TIMER_COUNT ;load end offset for end of frame timers - dec IntervalTimerControl ;decrement interval timer control, - bpl DecTimersLoop ;if not expired, only frame timers will decrement - lda #$14 - sta IntervalTimerControl ;if control for interval timers expired, - ldx #ALL_TIMER_COUNT ;interval timers will decrement along with frame timers - DecTimersLoop: - lda Timers,x ;check current timer - beq SkipExpTimer ;if current timer expired, branch to skip, - dec Timers,x ;otherwise decrement the current timer - SkipExpTimer: - dex ;move onto next timer - bpl DecTimersLoop ;do this until all timers are dealt with -NoDecTimers: + jsr DecrementMarioTimers ; Now do a few checks to see what state mario is in. jsr $903A ; link main @@ -1136,11 +1117,7 @@ NoDecTimers: jsr ProcStarPower jsr ProcTanookiStatue - lda SavedJoypadBits ;save the real A and B buttons for next frame. - and #%11000000 ;A_B_Buttons is zeroed while statued, so using it here - sta PreviousA_B_Buttons ;made B look freshly pressed when the statue ended - lda #$00 - sta Left_Right_Buttons ;nullify left and right buttons temp variable + jsr ProcessMarioInput ; Check scroll lock. If we are locked in, then prevent leaving the screen lda ScrollLock @@ -1171,6 +1148,41 @@ NoDecTimers: ; jsr CheckSideviewTransition ; this is already run during collision detection jmp PlayerGfxHandler ; rts +.reloc +.export DecrementMarioTimers +.proc DecrementMarioTimers + ; decrement timers here cause lazy + ; Move the timers ahead by a frame as well + lda TimerControl ;if master timer control not set, decrement + beq DecTimers ;all frame and interval timers + dec TimerControl + bne NoDecTimers + DecTimers: + ldx #FRAME_TIMER_COUNT ;load end offset for end of frame timers + dec IntervalTimerControl ;decrement interval timer control, + bpl DecTimersLoop ;if not expired, only frame timers will decrement + lda #$14 + sta IntervalTimerControl ;if control for interval timers expired, + ldx #ALL_TIMER_COUNT ;interval timers will decrement along with frame timers + DecTimersLoop: + lda Timers,x ;check current timer + beq SkipExpTimer ;if current timer expired, branch to skip, + dec Timers,x ;otherwise decrement the current timer + SkipExpTimer: + dex ;move onto next timer + bpl DecTimersLoop ;do this until all timers are dealt with +NoDecTimers: + rts +.endproc +.export ProcessMarioInput +.proc ProcessMarioInput + lda SavedJoypadBits ;save the real A and B buttons for next frame. + and #%11000000 ;A_B_Buttons is zeroed while statued, so using it here + sta PreviousA_B_Buttons ;made B look freshly pressed when the statue ended + lda #$00 + sta Left_Right_Buttons ;nullify left and right buttons temp variable + rts +.endproc .reloc SetPlayerDownstabbingHitbox: From 3c04b732add96a0a23988e80f25cc4c5db0dd5bf Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 18 Jul 2026 17:53:12 -0400 Subject: [PATCH 110/128] Shuffle a bunch and finish the basic title screen Gets a lot of code moved into bank E so we can use the rest of the banks for important non title screen things. Fixes a few outstanding issues with randomizer patches --- RandomizerCore/Asm/Init.s | 2 +- RandomizerCore/Asm/ram.inc | 1 + .../Asm/z2mario/CustomTitleScreen.s | 679 +++++++++++++----- RandomizerCore/Asm/z2mario/integration.s | 3 + RandomizerCore/Asm/z2mario/mario.s | 23 - RandomizerCore/Asm/z2mario/mario_title.chr | Bin 1024 -> 2048 bytes RandomizerCore/Hyrule.cs | 39 +- RandomizerCore/ROM.cs | 31 + 8 files changed, 535 insertions(+), 243 deletions(-) diff --git a/RandomizerCore/Asm/Init.s b/RandomizerCore/Asm/Init.s index 42699042d..79bc84ebe 100644 --- a/RandomizerCore/Asm/Init.s +++ b/RandomizerCore/Asm/Init.s @@ -33,7 +33,7 @@ ; FREE "PRG0" [$AA40, $c000) ;FREE "PRG0" [$A89E, $A980) -FREE "PRG0" [$A89E, $A970) ; Reserve some space for z2ft +; FREE "PRG0" [$A89E, $A970) ; Reserve some space for z2ft FREE "PRG0" [$AB00, $c000) ; give room for z2edit to patch $aa40 FREE "PRG1" [$87c6, $88a0) diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index 447699765..b59440edf 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -364,6 +364,7 @@ SavedDoorExit2X = $05d9 SmallDropCount = $05df LargeDropCount = $05e0 +HalfEncounterRateStepCounter = $06e0 Lives = $0700 NumberofLives = $0700 EntryFacing = $0701 ; Sideview entry facing direction. 0=Facing right (enter from the left), 1=Facing left (enter from the right) diff --git a/RandomizerCore/Asm/z2mario/CustomTitleScreen.s b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s index fc9756a9a..461e86f36 100644 --- a/RandomizerCore/Asm/z2mario/CustomTitleScreen.s +++ b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s @@ -1,7 +1,7 @@ .include "z2r.inc" .import GameRoutines, PlayerGfxHandler, DrawMetasprite, PlayerBankTable -.import SwapToPRG0, SwapToSavedPRG +.import SwapToPRG0, SwapToSavedPRG, SwapPRG .import ClearExtraRAMOnScreenTransition, ClearCollisionRAM, BankSwitchMarioCHR ;; Custom title screen for z2 mario @@ -10,7 +10,6 @@ ; makes the scroll split use irq instead ; redo all the star sprites to use a new custom twinkle code -.segment "PRG5" NUM_SKY_STARS = 25 NUM_WATER_STARS = 11 @@ -21,31 +20,52 @@ STAR_TILE_C2 = $ea STAR_TILE_C1 = $ec SHOOTING_STAR_TILE = STAR_TILE_C2 -seed = $6690 -StarColorIndex = $6691 -StarTimer = $66B5 -StarBaseTimer = $66D9 -ShootingStarY = $66FD -ShootingStarX = $66FE -ShootingStarActive = $66FF -TitleTempIndex = $6700 - -TitleIrqActive = $6701 +; Timers for the title screen created in z2ft +; since the original game synced to the music, z2ft +; recreated this effect with hand timed timers +TitleStage = $dc +TitleStageBit = $dd +TitleStageTimerLo = $de +TitleStageTimerHi = $df + +TitleScreenRamBase = $6690 +SET_RES_BASE TitleScreenRamBase +RESV seed +RESV StarColorIndex, NUM_STARS +RESV StarTimer, NUM_STARS +RESV StarBaseTimer, NUM_STARS +RESV ShootingStarY +RESV ShootingStarX +RESV ShootingStarActive +RESV TitleTempIndex +RESV TitleIrqActive ; Copies of the scroll data that will be written during IRQ this frame -TitleIrqPpuCtrl = $6702 -TitleIrqNametableHi = $6703 -TitleIrqNametableLo = $6704 +RESV TitleIrqPpuCtrl +RESV TitleIrqNametableHi +RESV TitleIrqNametableLo +RESV TitleState +RESV PipeYPositionLo +RESV PipeYPositionHi +RESV PipeXPosition + +; If the player jumps off the screen, we'll just have mario come out of the pipe again +; put this in ZP so that it gets cleared on boot, but not on loop +Respawning = $db TITLE_SPLIT_IRQ_LINE = 142 -; Spawn mario here for testing for now. -TITLE_MARIO_X = $78 -TITLE_MARIO_Y = $30 +TITLE_PIPE_RISE_TARGET_TIMER = $01f4 +TITLE_MARIO_RISE_TARGET_TIMER = $00f9 +TITLE_MARIO_LEAVES_TARGET_TIMER = $03d4 +TITLE_PIPE_TARGET_Y = 143 +TITLE_PIPE_X = 112 + FREE "PRG5" [$A7C1, $A932) +.segment "PRG5" .org $A72E jsr InitTitleStars @@ -61,146 +81,218 @@ FREE "PRG5" [$A7C1, $A932) ; Sprite/OAM work runs after setting the split IRQ so that timing-critical IRQ ; doesn't get delayed due to running the mario code .org $A737 - jsr SetTitleSplitIrq - jsr UpdateTitleSprites + jsr RunTitleMain jmp $A76A FREE_UNTIL $A76A .org $AB6D - jsr SetTitleSplitIrq - jsr UpdateTitleSprites + jsr RunTitleMain jmp $ABA0 FREE_UNTIL $ABA0 +.segment "PRG7" .reloc -SetTitleSplitIrq: - lda PpuCtrlShadow - and #$FC - ora $36 - sta TitleIrqPpuCtrl - lda $27 - sta TitleIrqNametableHi - lda $28 - sta TitleIrqNametableLo +RunTitleMain: + lda #$e ; use the same bank the extended + sta CurrentPrgBank + jsr SwapPRG + jsr SetTitleSplitIrq + jsr UpdateTitleSprites + lda #5 + sta CurrentPrgBank + jmp SwapPRG - ; write JMP IrqHdlr to RAM - lda #$4c - sta IRQTrampoline - lda #TitleSplitIrqBody - sta IRQTrampolineAddrHi - lda #TITLE_SPLIT_IRQ_LINE - sta LineIrqTgtReg - lda #1 - sta TitleIrqActive - lda #ENABLE_SCANLINE_IRQ - sta LineIrqStatusReg - cli +.segment "PRG1C", "PRG1D" + +; Fake that mario doesn't collide while in the title screen (for z2 collision that is) +.org $850c + clc rts +; Main loop for the title screen driven by TitleState. .reloc -TitleSplitIrqBody: - pha - ; burn some cycles to hide the scroll split - 29 cycles for 6 bytes - clc - lda #$2a - nop - bcc *-2 - lda TitleIrqPpuCtrl - sta PpuCtrlShadow - sta PPUCTRL - lda TitleIrqNametableHi - sta PPUADDR - lda TitleIrqNametableLo - sta PPUADDR - lda PPUDATA - lda PPUDATA - lda PPUSTATUS - lda #0 - sta TitleIrqActive - sta LineIrqStatusReg - pla - rti +UpdateTitleSprites: + lda #0 + sta CurrentOAMOffset + jsr RunCurrentTitleState -; New palette for the stars -.org $AF51 - .byte $10, $30, $21 -.org $AF59 - .byte $16, $27, $18 -; The vanilla title screen uses Player_Y_Position ($29) to hold part of the scroll -; so relocate all of those to use $3a instead. -.org $AD09 - .byte $3A -.org $AD2B - .byte $3A -.org $AD2F - .byte $3A -.org $AD47 - .byte $3A -.org $AD53 - .byte $3A -.org $AD59 - .byte $3A -.org $ADE7 - .byte $3A + jsr DrawTitleRocks + jsr DrawTitleStars + ; Hide whatever OAM slots are left unclaimed this frame. + ldx CurrentOAMOffset + beq @Done +@HideRemaining: + lda #$F8 + sta Sprite_Y_Position,x + inx + inx + inx + inx + bne @HideRemaining +@Done: + rts .reloc -RockOamTable: - .byte $7F, $F0, $F8 - .byte $3F, $EE, $F8 - .byte $4F, $F6, $F0 - .byte $4F, $F4, $F8 - .byte $5F, $EE, $E8 - .byte $5F, $F0, $F0 - .byte $5F, $F0, $F8 - ; .byte $6F, $EE, $D0 - .byte $6F, $F0, $D8 - .byte $6F, $F4, $E0 - .byte $6F, $F2, $E8 - .byte $6F, $F6, $F0 - .byte $6F, $F0, $F8 - ; .byte $7F, $F0, $D0 - .byte $7F, $F2, $D8 - .byte $7F, $F4, $E0 - .byte $7F, $F0, $E8 - .byte $7F, $F2, $F0 -ROCK_TABLE_LEN = (* - RockOamTable) +RunCurrentTitleState: + lda TitleState + asl + tax + lda TitleStateTable+1,x + pha + lda TitleStateTable,x + pha + rts -StarPosTableY: - .byte $07, $07, $0F, $0F, $5F, $17, $1F, $1F, $5F, $1F, $4F, $1F, $1F - .byte $27, $27, $27, $2F, $2F, $37, $37, $3F, $3F, $47, $47, $4F ; sky (25) - .byte $AF, $B4, $9F, $AD, $92, $95, $8F, $95, $98, $78, $93 ; water (11) +.reloc +TitleStateTable: + .word WaitForFirstSpawn-1 + .word CheckForSpawnMario-1 + .word PipeRises-1 + .word MarioRises-1 + .word MarioLeavesScreen-1 -StarPosTableX: - .byte $50, $A0, $20, $78, $40, $E0, $10, $28, $60, $A0, $80, $E0, $E8 - .byte $78, $C8, $E8, $28, $D8, $10, $C8, $A8, $18, $08, $48, $E0 ; sky (25) - .byte $40, $20, $58, $48, $70, $88, $90, $B0, $8A, $95, $8E ; water (11) +.reloc +WaitForFirstSpawn: + lda TitleStage + cmp #2 + bne + + ; Move to the next state + inc TitleState + + + rts -TileForColor: - .byte STAR_TILE_C1, STAR_TILE_C2, STAR_TILE_C3 +.reloc +CheckForSpawnMario: + ; Only respawn mario if he's off the screen + lda Player_Y_HighPos + cmp #1 + beq + + inc TitleState + + + jsr DrawTitleMario + jmp DrawTitlePipe .reloc -BarebonesTitleScreenRNG: - lda seed - asl a - bcs @NoEor - eor #$46 -@NoEor: - adc #$EB - sta seed +PipeRises: + ; Disable the player control temporarily except start + lda SavedJoypadBits + and #$10 + sta SavedJoypadBits + ; If the pipe is onscreen, keep rising + lda PipeYPositionHi + cmp #1 + beq @continueRise + ; If we are in stage 2, then wait for the specific timer + ; to sync with the first title launch. Otherwise we can respawn whenever + jsr CheckTimerByState + bcc @noSpawn + ; spawn the pipe! + lda #1 + sta PipeYPositionHi + lda #TITLE_PIPE_TARGET_Y + 32 + sta PipeYPositionLo + jmp DrawTitlePipe +@continueRise: + lda PipeYPositionLo + cmp #TITLE_PIPE_TARGET_Y + beq @risefinished + ; rise at 1 px / 4 frames + lda FrameCounter + and #3 + bne + + dec PipeYPositionLo + + + jmp DrawTitlePipe +@risefinished: + ; reached our target spot so exit. This lets future runs keep the pipe on the screen + inc TitleState + jmp DrawTitlePipe +@noSpawn: + rts + + +.reloc +MarioRises: + jsr DrawTitlePipe + ; Disable the player control temporarily except start + lda SavedJoypadBits + and #$10 + sta SavedJoypadBits + + lda Player_Y_HighPos + cmp #1 + beq @continueRising + ; check if its time to start rising mario + jsr CheckTimerByState + bcc @noSpawn + ; Time to move mario into position + ldx #1 + stx Player_Y_HighPos + jsr InitMarioData + jmp DrawTitleMario + +@continueRising: + ; check if we need to keep rising mario + lda FrameCounter + and #3 + bne + + dec Player_Y_Position + lda Player_Y_Position + cmp #TITLE_PIPE_TARGET_Y - 31 + bne + + ; reached our target spot + inc TitleState + + + jmp DrawTitleMario +@noSpawn: rts +.reloc +MarioLeavesScreen: + ; Disable the player control temporarily except start +; lda SavedJoypadBits +; and #$10 +; sta SavedJoypadBits + +; lda FrameCounter +; and #3 +; bne + +; dec Player_Y_Position +; lda Player_Y_Position +; + + ; For now, just turn on respawning and let the player move + lda #1 + sta TitleState + sta Respawning + jsr DrawTitleMario + jmp DrawTitlePipe + +.reloc +TitleTimerStage: + .byte 2, 2, 4 +.reloc +TitleTimerTargetTimerLo: + .byte TITLE_PIPE_RISE_TARGET_TIMER, >TITLE_MARIO_RISE_TARGET_TIMER, >TITLE_MARIO_LEAVES_TARGET_TIMER + ; Initialize the star timers and colors. The old stars/water sparkles worked by cycling through ; different palette indicies, so they would use each of the colors from color 3 in the different palettes ; Well thats a problem when i want to use mario on the screen, and stars shouldn't twinkle brown. ; So instead, we lose one color from the star twinkle, (the deep $02 blue from before), and instead ; put all stars on the same palette index 0. We then use two different "star sprites" that use color ; 1 and 2 as well as the vanilla color 3. That way the stars can twinkle independently of each other. +.segment "PRG5" .reloc InitTitleStars: + lda Respawning + beq + + ; don't reinitalize if we have looped once + rts + + lda FrameCounter sta seed @@ -245,19 +337,20 @@ InitTitleStars: lda #0 sta ShootingStarActive sta TitleIrqActive + sta TitleState ; clear z2 collision ram ldy #$d0 lda #$11 ; value chosen to not match any "lava" or other weird tiles -@loop: +@collisionloop: sta $6000-1,y sta $6000-1+$d0,y sta $6000-1+$d0*2,y sta $6000-1+$d0*3,y dey - bne @loop + bne @collisionloop - jmp SetupTitleMario + jmp SetupTitleMario ; jumping here to split the function so it fits better .reloc SetupTitleMario: @@ -265,36 +358,22 @@ SetupTitleMario: jsr ClearExtraRAMOnScreenTransition jsr ClearCollisionRAM - lda #8 ; PlayerCtrlRoutine index in the GameRoutines jump table - sta GameEngineSubroutine lda #$28 ;store value here sta VerticalForceDown ;for fractional movement downwards ; Set our custom floor bytes ldx #0 - stx Player_State - stx PlayerSize ; big mario - stx SwimmingFlag - stx CrouchingFlag - stx Player_X_Speed - stx Player_Y_Speed - stx Player_Y_MoveForce - stx LinkCollisionBits - inx - stx Player_Y_HighPos ; page 1 == onscreen - stx PlayerFacingDir - stx Player_MovingDir - stx Player_PageLoc - stx ScreenLeft_X_Pos - stx ScreenLeft_PageLoc + stx Respawning + jsr InitMarioData inx + ; page 1 == onscreen, so put him on page 2 offscreen + ; falling forever (until its time to spawn) + stx Player_Y_HighPos stx Player_SprAttrib - - lda #TITLE_MARIO_X - sta Player_X_Position - lda #TITLE_MARIO_Y - sta Player_Y_Position + stx PipeYPositionHi ldx #8 + ; PlayerCtrlRoutine index in the GameRoutines jump table + stx GameEngineSubroutine @collisionLoop: sta $63d7,x sta $63e7+$d0,x @@ -320,57 +399,62 @@ SetupTitleMario: sbc #16 tax bpl @loop2 - - lda #CHR_BIGMARIO sta PlayerChrBank inc ReloadCHRBank jmp BankSwitchMarioCHR .reloc -UpdateTitleSprites: - lda #0 - sta CurrentOAMOffset - - jsr DrawTitleOverlay - jsr DrawTitlePipe - jsr DrawTitleMario - jsr DrawTitleRocks - jsr DrawTitleStars - - ; Hide whatever OAM slots are left unclaimed this frame. - ldx CurrentOAMOffset - beq @Done -@HideRemaining: - lda #$F8 - sta Sprite_Y_Position,x - inx - inx - inx - inx - bne @HideRemaining -@Done: +BarebonesTitleScreenRNG: + lda seed + asl a + bcs @NoEor + eor #$46 +@NoEor: + adc #$EB + sta seed rts -.reloc -DrawTitleOverlay: - ; TODO: sprites that mask the pipe rising/lowering out of the ground. - rts +;; --- +; Drawing routines +.segment "PRG7" .reloc -DrawTitlePipe: - ; TODO: the pipe itself. +InitMarioData: + ldx #0 + stx Player_State + stx PlayerSize ; big mario + stx SwimmingFlag + stx CrouchingFlag + stx Player_X_Speed + stx Player_Y_Speed + stx Player_X_MoveForce + stx Player_Y_MoveForce + stx LinkCollisionBits + inx + stx PlayerFacingDir + stx Player_MovingDir + stx Player_PageLoc + stx ScreenLeft_X_Pos + stx ScreenLeft_PageLoc + lda #TITLE_PIPE_X+1 + sta Player_X_Position + lda #TITLE_PIPE_TARGET_Y + sta Player_Y_Position rts -.segment "PRG7" .reloc DrawTitleMario: - lda #5 - sta CurrentPrgBank jsr SwapToPRG0 .import DecrementMarioTimers, ProcessMarioInput - jsr DecrementMarioTimers - jsr GameRoutines + + lda TitleState + cmp #3 ; MarioRises skip running mario code during that to keep him from getting ejected out of the pipe + beq + + jsr DecrementMarioTimers + jsr GameRoutines + + + jsr PlayerGfxHandler jsr ProcessMarioInput @@ -388,7 +472,7 @@ DrawTitleMario: jsr DrawMetasprite jmp SwapToSavedPRG -.segment "PRG5" +.segment "PRG1C", "PRG1D" .reloc DrawTitleRocks: ldx CurrentOAMOffset @@ -414,6 +498,111 @@ DrawTitleRocks: stx CurrentOAMOffset rts +.reloc +PipeSpriteXData: + .byte TITLE_PIPE_X+0, TITLE_PIPE_X+8, TITLE_PIPE_X+16, TITLE_PIPE_X+24 +.reloc +PipeSpriteTileTopRow: + .byte $ae+0, $ae+2, $ae+4, $ae+6 +.reloc +PipeSpriteTileBotRow: + .byte $b6+0, $b6+2, $b6+4, $b6+6 +.reloc +DrawTitlePipe: + lda PipeYPositionHi ; don't draw pipe if its hidden + cmp #1 + beq + + rts + + + ldy CurrentOAMOffset + ; check if we are already at the target + lda PipeYPositionLo + cmp #TITLE_PIPE_TARGET_Y + beq + + ; if not, draw a row of overlay sprites to hide the bottom + ldx #3 +@overlayloop: + lda PipeSpriteXData,x + sta Sprite_X_Position,y + lda #TITLE_PIPE_TARGET_Y+32 + sta Sprite_Y_Position,y + lda #$ba + sta Sprite_Tilenumber,y + lda #$21 ; background it to force the pipe to hide + sta Sprite_Attributes,y + iny + iny + iny + iny + dex + bpl @overlayloop + + + ; Draw the top row of tiles. This one is always visible + ldx #3 +@toploop: + lda PipeSpriteXData,x + sta Sprite_X_Position,y + lda PipeYPositionLo + sta Sprite_Y_Position,y + lda PipeSpriteTileTopRow,x + sta Sprite_Tilenumber,y + lda #$03 + sta Sprite_Attributes,y + iny + iny + iny + iny + dex + bpl @toploop + ; bottom row, only display this is the pipe is high enough + lda PipeYPositionLo + cmp #TITLE_PIPE_TARGET_Y+16 + bcs @exit + ; carry is clear + adc #16 + sta $00 + ldx #3 +@botloop: + lda PipeSpriteXData,x + sta Sprite_X_Position,y + lda $00 + sta Sprite_Y_Position,y + lda PipeSpriteTileBotRow,x + sta Sprite_Tilenumber,y + lda #$03 + sta Sprite_Attributes,y + iny + iny + iny + iny + dex + bpl @botloop +@exit: + sty CurrentOAMOffset + rts + +.reloc +CheckTimerByState: + ; Skip checking timers if this is a respawn + lda Respawning + bne @yesSpawn + ldx TitleState + lda TitleStage + cmp TitleTimerStage-2,x + bne @noSpawn + lda TitleStageTimerLo + cmp TitleTimerTargetTimerLo-2,x + bne @noSpawn + lda TitleStageTimerHi + cmp TitleTimerTargetTimerHi-2,x + bne @noSpawn +@yesSpawn: + sec + rts +@noSpawn: + clc + rts + .reloc DrawTitleStars: ldy CurrentOAMOffset @@ -504,3 +693,119 @@ DrawTitleStars: bne @StarLoop sty CurrentOAMOffset rts + +; ---- +; IRQ handler +.reloc +SetTitleSplitIrq: + lda PpuCtrlShadow + and #$FC + ora $36 + sta TitleIrqPpuCtrl + lda $27 + sta TitleIrqNametableHi + lda $28 + sta TitleIrqNametableLo + + ; write JMP IrqHdlr to RAM + lda #$4c + sta IRQTrampoline + lda #TitleSplitIrqBody + sta IRQTrampolineAddrHi + lda #TITLE_SPLIT_IRQ_LINE + sta LineIrqTgtReg + lda #1 + sta TitleIrqActive + lda #ENABLE_SCANLINE_IRQ + sta LineIrqStatusReg + cli + rts + +.reloc +RockOamTable: + .byte $7F, $F0, $F8 + .byte $3F, $EE, $F8 + .byte $4F, $F6, $F0 + .byte $4F, $F4, $F8 + .byte $5F, $EE, $E8 + .byte $5F, $F0, $F0 + .byte $5F, $F0, $F8 + ; .byte $6F, $EE, $D0 + .byte $6F, $F0, $D8 + .byte $6F, $F4, $E0 + .byte $6F, $F2, $E8 + .byte $6F, $F6, $F0 + .byte $6F, $F0, $F8 + ; .byte $7F, $F0, $D0 + .byte $7F, $F2, $D8 + .byte $7F, $F4, $E0 + .byte $7F, $F0, $E8 + .byte $7F, $F2, $F0 +ROCK_TABLE_LEN = (* - RockOamTable) + +StarPosTableY: + .byte $07, $07, $0F, $0F, $5F, $17, $1F, $1F, $5F, $1F, $4F, $1F, $1F + .byte $27, $27, $27, $2F, $2F, $37, $37, $3F, $3F, $47, $47, $4F ; sky (25) + .byte $AF, $B4, $9F, $AD, $92, $95, $8F, $95, $98, $78, $93 ; water (11) + +StarPosTableX: + .byte $50, $A0, $20, $78, $40, $E0, $10, $28, $60, $A0, $80, $E0, $E8 + .byte $78, $C8, $E8, $28, $D8, $10, $C8, $A8, $18, $08, $48, $E0 ; sky (25) + .byte $40, $20, $58, $48, $70, $88, $90, $B0, $8A, $95, $8E ; water (11) + +TileForColor: + .byte STAR_TILE_C1, STAR_TILE_C2, STAR_TILE_C3 + + +.segment "PRG7" +.reloc +TitleSplitIrqBody: + pha + ; burn some cycles to hide the scroll split - 29 cycles for 6 bytes + clc + lda #$2a + nop + bcc *-2 + lda TitleIrqPpuCtrl + sta PpuCtrlShadow + sta PPUCTRL + lda TitleIrqNametableHi + sta PPUADDR + lda TitleIrqNametableLo + sta PPUADDR + lda PPUDATA + lda PPUDATA + lda PPUSTATUS + lda #0 + sta TitleIrqActive + sta LineIrqStatusReg + pla + rti + +.segment "PRG5" + +; New palette for the stars +.org $AF51 + .byte $10, $30, $21 +.org $AF59 + .byte $16, $27, $18 +.org $AF5d + .byte $29, $1a, $0f +; The vanilla title screen uses Player_Y_Position ($29) to hold part of the scroll +; so relocate all of those to use $3a instead. +.org $AD09 + .byte $3A +.org $AD2B + .byte $3A +.org $AD2F + .byte $3A +.org $AD47 + .byte $3A +.org $AD53 + .byte $3A +.org $AD59 + .byte $3A +.org $ADE7 + .byte $3A diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index a066934a1..b73c70af9 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -184,6 +184,9 @@ ApplySpellEffectButPreventBothFireAndJump: @apply: rts +.org $8E8A + rts ; Disable the vanilla spell spell behavior that turns everything to bots + .org $8fe3 jmp *+3 ; Skip duplicate draw??? .reloc diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index aa4605c91..fac8db712 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -22,29 +22,6 @@ STAR_WARNING_FRAMES = 32 ; palette cycle slows down below this ;------------------------------------------------------------------------------------- .proc GameRoutines -;.ifdef DEMO_CODE - ; lda #1 - ; sta $0785 ;candle - ; sta $078c ;key - ; sta $078b ;hammer - ; sta $0788 ;boots - ; sta $0787 ;raft - ; sta $077b ; shield spell - ; sta $077c ; jump spell - ; sta $077d ; life spell - ; sta $077e ; fairy spell - ; sta $077f ; fire spell - ; sta $0780 ; reflect spell - ; sta $0781 ; spell spell - ; sta $0782 ; thunder spell - ; lda #8 - ; sta $0783 ; magic jar count - ; sta $0778 ; magic level - -; lda #$ff -; sta $0773 ;infinite magic -; sta $0774 ;infinite health -;.endif ; check for going down the chimney in darunia lda $070E ; chimney sinking state (0 == not sinking non zero == sink away) diff --git a/RandomizerCore/Asm/z2mario/mario_title.chr b/RandomizerCore/Asm/z2mario/mario_title.chr index 1f9233dcfb35c01a94cc0cb2e8f2a406bc3b18d0..220c206ee4194eca6cee3d8b6bb3ff3791022c1d 100644 GIT binary patch delta 297 zcmZqRXb@PyQvd&d^JftF-|(jg1i+ln|Lwt?p8xj$|9|)f0{&zwUwIJ-L2MxJ4~Ttn<^TFWHc+4p1?c?WP{0BO=zNIv j3{dM~4nXkt#6Y0S5(q@)Gr268Jdrtn@&e`r95@gFtc#WD delta 6 NcmZn=Xy91D0sscG0lfeK diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 71caeb969..1db5f418e 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -2464,6 +2464,8 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra var title_graphics = Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.z2mario.mario_title.chr"); rom.Put(ROM.ChrRomOffset + 0x1380, title_graphics[0..0x300]); rom.Put(ROM.ChrRomOffset + 0x1840, title_graphics[0x300..0x400]); + // The title graphics also include a pipe now, place that somewhere we can use it. + rom.Put(ROM.ChrRomOffset + 0x0ae0, title_graphics[0x400..0x500]); // I rewrote the star sparkle animation on the title screen to use different star sprites instead // I build them from copying the existing star sprite var starSpriteTile = rom.GetBytes(ROM.ChrRomOffset + 0x0e80, 0x20); @@ -2481,38 +2483,6 @@ private void RandomizeStartingValues(RandomizerProperties props, Assembler a, Ra rom.Put(0x294, 0x60); //skips the whole routine } - if (props.EncounterRates == EncounterRate.HALF) - { - //terrain timers - rom.Put(0x250, 0x40); // grass - rom.Put(0x251, 0x30); // desert - rom.Put(0x252, 0x30); // forest - rom.Put(0x253, 0x40); - rom.Put(0x254, 0x12); - rom.Put(0x255, 0x06); - - //initial overworld timer - rom.Put(0x88A, 0x10); - - /* - * insert jump to a8aa at 2a3 (4c AA A8) - * - * At a8aa - * Load $26 (A5 26) - * bne to end (2 bytes) (D0 0D) - * inc new step counter (where?) EE E0 06 - * Load 1 to accumulator (A9 01) - * xor new step counter with 1 (2D E0 06) - * bne to end (D0 03) - * jump to encounter spawn 8298 (4C 98 82) - * jump to rts 829f (4C 93 82) - */ - rom.Put(0x29f, new byte[] { 0x4C, 0xAA, 0xA8 }); - - rom.Put(0x28ba, new byte[] { 0xA5, 0x26, 0xD0, 0x0D, 0xEE, 0xE0, 0x06, 0xA9, 0x01, 0x2D, 0xE0, 0x06, 0xD0, 0x03, 0x4C, 0x98, 0x82, 0x4C, 0x93, 0x82 }); - } - - if (props.ShuffleLifeRefill) { int lifeRefill = r.Next(1, 6); @@ -3915,6 +3885,11 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando } } + if (props.EncounterRates == EncounterRate.HALF) + { + rom.HalfEncounterRate(engine); + } + if (props.AttackEffectiveness == AttackEffectiveness.OHKO) { rom.UseOHKOMode(engine); diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index f69250223..63cccec33 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -2006,6 +2006,37 @@ public void FixBigBubbleSplit(Assembler asm, StatRandomizer randomizedStats) a.Byt(splitHp); } + public void HalfEncounterRate(Assembler asm) + { + var a = asm.Module(); + a.Code(/* lang=s */""" +.include "z2r.inc" +.segment "PRG0" +; Update terrain timers +.org $8240 + ; grass, desert, forset, ... etc + .byte $40, $30, $30, $40, $12, $06 +; Initial overworld timer +.org $887a + .byte $10 +; Update some new whatever this is +.org $828f + jmp PatchEncounterRate +FREE_UNTIL $8293 +.reloc +PatchEncounterRate: + lda $26 + bne @end + inc HalfEncounterRateStepCounter + lda #1 + eor HalfEncounterRateStepCounter + bne @end + jmp $8298 +@end: + jmp $8293 +"""); + } + public void FixItemPickup(Assembler asm) { // In Z2R, Link never holds items above his head. So, From d283708646685da5c0eff49609691bc1f3608ee4 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sat, 18 Jul 2026 23:38:27 -0400 Subject: [PATCH 111/128] Update IRQ to prevent horrible things from happening doing lda PPUDATA is very very cursed. --- .../Asm/z2mario/CustomTitleScreen.s | 55 +++++++++---------- RandomizerCore/Hyrule.cs | 2 +- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/CustomTitleScreen.s b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s index 461e86f36..dee47d98f 100644 --- a/RandomizerCore/Asm/z2mario/CustomTitleScreen.s +++ b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s @@ -41,9 +41,7 @@ RESV TitleTempIndex RESV TitleIrqActive ; Copies of the scroll data that will be written during IRQ this frame -RESV TitleIrqPpuCtrl -RESV TitleIrqNametableHi -RESV TitleIrqNametableLo +RESV TitleIrqYPosition RESV TitleState RESV PipeYPositionLo @@ -698,14 +696,9 @@ DrawTitleStars: ; IRQ handler .reloc SetTitleSplitIrq: - lda PpuCtrlShadow - and #$FC - ora $36 - sta TitleIrqPpuCtrl lda $27 - sta TitleIrqNametableHi - lda $28 - sta TitleIrqNametableLo + and #$0f + sta TitleIrqYPosition ; write JMP IrqHdlr to RAM lda #$4c @@ -732,13 +725,13 @@ RockOamTable: .byte $5F, $EE, $E8 .byte $5F, $F0, $F0 .byte $5F, $F0, $F8 - ; .byte $6F, $EE, $D0 + .byte $6F, $EE, $D0 .byte $6F, $F0, $D8 .byte $6F, $F4, $E0 .byte $6F, $F2, $E8 .byte $6F, $F6, $F0 .byte $6F, $F0, $F8 - ; .byte $7F, $F0, $D0 + .byte $7F, $F0, $D0 .byte $7F, $F2, $D8 .byte $7F, $F4, $E0 .byte $7F, $F0, $E8 @@ -763,26 +756,32 @@ TileForColor: .reloc TitleSplitIrqBody: pha - ; burn some cycles to hide the scroll split - 29 cycles for 6 bytes - clc - lda #$2a - nop - bcc *-2 - lda TitleIrqPpuCtrl - sta PpuCtrlShadow - sta PPUCTRL - lda TitleIrqNametableHi - sta PPUADDR - lda TitleIrqNametableLo - sta PPUADDR - lda PPUDATA - lda PPUDATA - lda PPUSTATUS - lda #0 +; the vanilla z2 split is EXTREMELY CURSED +; if i put emojis in my source code i would surround that with skull emojis. +; it does two PPUDATA *reads* which destroys the mmc5. +lda TitleIrqYPosition +sta $2006 + +lda #43 +jsr delay_a_27_clocks +lda #0 +sta $2006 + sta TitleIrqActive sta LineIrqStatusReg pla rti +delay_a_27_clocks: + sec +@L: sbc #5 + bcs @L ; 6 6 6 6 6 FB FC FD FE FF + adc #3 ; 2 2 2 2 2 FE FF 00 01 02 + bcc @4 ; 3 3 2 2 2 FE FF 00 01 02 + lsr ; - - 2 2 2 -- -- 00 00 01 + beq @5 ; - - 3 3 2 -- -- 00 00 01 +@4: lsr ; 2 2 - - 2 7F 7F -- -- 00 +@5: bcs @6 ; 2 3 2 3 2 7F 7F 00 00 00 +@6: rts ; .segment "PRG5" diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index 1db5f418e..f96f8dbb4 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -546,7 +546,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC if (Z2MarioModeEnabled && !IS_RANDOMIZED) { - z2Hash = Util.ToGameText("VER. 1.2.0 "); + z2Hash = Util.ToGameText("VER. 2.0.0 "); } ROMData.Put(0x17C2C, z2Hash); Hash = Util.FromGameText(z2Hash); From 87c5000d4642934c149b2179cd8d16641ac04fad Mon Sep 17 00:00:00 2001 From: James Rowe Date: Sun, 19 Jul 2026 00:02:39 -0400 Subject: [PATCH 112/128] Make hammer throw from mario at small height when small --- RandomizerCore/Asm/z2mario/integration.s | 5 ++++- RandomizerCore/Asm/z2mario/mario.s | 28 +++++++++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index b73c70af9..9ad671e96 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -18,7 +18,7 @@ STATUE_DURATION = $C0 STATUE_CHANGE_DURATION = $17 STATUE_BOUNCE_FORCE = $60 STATUE_FALL_RESTORE = $70 -STAR_DURATION = 180 ; 3 seconds arbitrarily chosen because i like it +STAR_DURATION = 240 ; 4 seconds arbitrarily chosen because i like it .segment "PRG7" SpellCastingRoutine = $8DC3 ; Link Main @@ -1201,6 +1201,9 @@ SetPlayerDownstabbingHitbox: ; lda Player_State ; cmp #1 ; bne @notstabbing + ; don't stomp on things while star power is running + ; lda StarInvincibleTimer + ; bne @notstabbing lda Player_Y_Speed bmi @notstabbing beq @notstabbing diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index fac8db712..403a123a9 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -2537,22 +2537,25 @@ GetProperObjOffset: lda SavedJoypadBits and #B_Button | Up_Dir ;check for up + b button pressed cmp #B_Button | Up_Dir - bne UpdateHammers ;branch if not pressed + bne @UpdateHammersLocal ;branch if not pressed and #B_Button ;check if b button just pressed and PreviousA_B_Buttons - bne UpdateHammers ;if button pressed in previous frame, branch + bne @UpdateHammersLocal ;if button pressed in previous frame, branch ldy Player_Y_HighPos ;if player too high or too low, branch dey - bne UpdateHammers + bne @UpdateHammersLocal ldx #0 ; Check to see if we have an open fireball spot to put the hammer in lda Fireball_State,x ;load fireball state - beq SpawnHammer ;if not inactive, branch + beq @SpawnHammer ;if not inactive, branch inx lda Fireball_State,x ;load fireball state - bne UpdateHammers ;if not inactive, branch -SpawnHammer: + beq @SpawnHammer + ;if not inactive, branch +@UpdateHammersLocal: + jmp UpdateHammers +@SpawnHammer: ; stat tracking consider a hammer a HiStab inc StatHiStabCount+0 bne @Skip @@ -2578,6 +2581,18 @@ SpawnHammer: lda Player_PageLoc ;get player's page location adc #0 sta Fireball_PageLoc,x + ; adjust offset for small mario + lda PlayerSize + beq @big + lda Player_Y_Position ;get player's vertical position and store + clc + adc #$08 + sta Fireball_Y_Position,x + lda #$01 ;set high byte of vertical position + adc #0 + sta Fireball_Y_HighPos,x + jmp @next + @big: lda Player_Y_Position ;get player's vertical position and store sec sbc #$08 @@ -2585,6 +2600,7 @@ SpawnHammer: lda #$01 ;set high byte of vertical position sbc #0 sta Fireball_Y_HighPos,x + @next: tya ; Z2 needs a value set for the knockback value in $6d sta Fireball_MovingDir,x dey ;decrement to use as offset here From f7f7032bb213d69d3f900752426a4acdebb55391 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 20 Jul 2026 14:21:13 -0400 Subject: [PATCH 113/128] Add missing jump in palace warp --- RandomizerCore/Asm/Flute.s | 1 + 1 file changed, 1 insertion(+) diff --git a/RandomizerCore/Asm/Flute.s b/RandomizerCore/Asm/Flute.s index 3977357e2..90918eb29 100644 --- a/RandomizerCore/Asm/Flute.s +++ b/RandomizerCore/Asm/Flute.s @@ -271,6 +271,7 @@ TwisterWarp: lda FluteWarpFlags and PowersOfTwo,y bne @found_dst +@skip_dst: dex bpl @next_dst ; nothing visited yet, so leave From 0b4e038d2696980475ed923a63cdc60ecc2d283c Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 20 Jul 2026 14:50:56 -0400 Subject: [PATCH 114/128] Fix almost all sfx issues by better clearing sfx state Also make the tanooki statue bounce heavier --- RandomizerCore/Asm/z2mario/integration.s | 98 +++++++++++++++++------- RandomizerCore/Asm/z2mario/sfx.s | 74 ++++++++++++++---- 2 files changed, 131 insertions(+), 41 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 9ad671e96..53198e300 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -4,6 +4,8 @@ .import GameRoutines, ProcFireball_Bubble, PlayerGfxHandler, DrawMetasprite .import ProcHammerTime, ProcStarPower .import Square1SfxHandler, Square2SfxHandler, NoiseSfxHandler +.import ResetSquare1Sfx, ResetSquare2Sfx, ResetMarioSfxState +.import ReleaseSquare1Sfx, ReleaseSquare2Sfx .import SwapToSavedPRG, SwapToPRG0 .export SlightlyModifiedCollisionRoutine .export BankSwitchMarioCHR, ClearExtraRAMOnScreenTransition, ClearCollisionRAM @@ -16,7 +18,7 @@ FREE "PRG0" [$9815, $9924) STATUE_DURATION = $C0 STATUE_CHANGE_DURATION = $17 -STATUE_BOUNCE_FORCE = $60 +STATUE_BOUNCE_FORCE = $bf STATUE_FALL_RESTORE = $70 STAR_DURATION = 240 ; 4 seconds arbitrarily chosen because i like it @@ -1582,6 +1584,17 @@ UPDATE_BYTE $02 @ $0e9e ;------------------------------------------------------------------------------------- .segment "PRG6" + +; Patch the "Silent" sound effect so that we clear any of the existing mario sound effects +; when it starts. When silent, the z2 engine stops clocking the sweeps, leading to all sorts +; of buggy sounds, so this just cuts it so we don't get the bad sfx +.org $9006 + jmp MarioSoundOff +.reloc +MarioSoundOff: + sta $4015 + jmp ResetMarioSfxState + .org $9016 jsr CheckMarioSq1Sfx jsr CheckMarioSq2Sfx @@ -1592,50 +1605,81 @@ UPDATE_BYTE $02 @ $0e9e nop .reloc ClearQueues: + ; Clear the original Z2 queue we patched over lda #0 + sta Z2Square1SoundQueue + ; and also the mario queues sta Square1SoundQueue sta Square2SoundQueue sta NoiseSoundQueue rts +; Implements a small priority queue for sfx on sq 1. .reloc CheckMarioSq1Sfx: - lda $07E1 ; L990B active effect type (non-zero = L990B is driving Square 1) - bne @ActualZ1Sfx - jsr Square1SfxHandler - lda Square1SoundBuffer - beq @PlayingZ2 - lda #$ff - sta Z2Square1SoundBuffer - rts -@PlayingZ2: - lda #$ff - cmp Z2Square1SoundBuffer - bne @ActualZ1Sfx - ; this is the fake sfx id we used to signal that this was a mario buffer - ; but now we can clear it. + ; $ff in the Z2 buffer is our own "mario owned square 1 last frame" marker, + ; if its set, then we don't want z2 to run any sfx processing, so we clear the z2 buffer here. + lda Z2Square1SoundBuffer + cmp #$ff + bne + lda #0 sta Z2Square1SoundBuffer - rts + + + ; Check for "active" effects like the spell cast sound which are in $7e1 instead. + ; We want these to take priority over mario noises. + lda $07E1 + bne @LoseToEffect + ; z2 is wanting to start a sfx this frame, and we aren't playing anything, so let it happen. + ; this keeps mario sfx from cutting off important things like item drop noises. + lda Z2Square1SoundQueue + bne @YieldToZ2 + + ; We already have a z2 sfx running on the sq1 channel, so don't let a mario sfx start + lda Z2Square1SoundBuffer + bne @ActualZ1Sfx + ; check to see if we have anything to start for mario sfx + jsr Square1SfxHandler + lda Square1SoundBuffer + beq @ActualZ1Sfx + ; and if we did get something started, set the flag to prevent z2 sfx next frame + lda #$ff + sta Z2Square1SoundBuffer + rts +@LoseToEffect: + ; A z2 active effect is starting, so cancel the smb1 sfx and run the z2 sfx code + jsr ReleaseSquare1Sfx + jmp @ActualZ1Sfx +@YieldToZ2: + ; nothing of ours to tear down? then leave smb1 alone + lda Square1SoundBuffer + beq @ActualZ1Sfx + jsr ResetSquare1Sfx @ActualZ1Sfx: jmp $92F4 ; Original square 1 sfx from z2 + .reloc CheckMarioSq2Sfx: + ; We don't need as complicated of a structure for smb1 square 2 sfx since we run almost all + ; sfx on square 1. We still do some juggling, but we don't have active effects here. + lda Z2Square2SoundBuffer + cmp #$ff + bne + + lda #0 + sta Z2Square2SoundBuffer + + + lda Z2Square2SoundQueue + bne @YieldToZ2 jsr Square2SfxHandler lda Square2SoundBuffer - beq @PlayingZ2 + beq @ActualZ2Sfx lda #$ff sta Z2Square2SoundBuffer rts -@PlayingZ2: - lda #$ff - cmp Z2Square2SoundBuffer - bne @ActualZ2Sfx - ; this is the fake sfx id we used to signal that this was a mario buffer - ; but now we can clear it. - lda #0 - sta Z2Square2SoundBuffer - rts +@YieldToZ2: + ; Clear the mario sfx if something is happening + lda Square2SoundBuffer + beq @ActualZ2Sfx + jsr ResetSquare2Sfx @ActualZ2Sfx: jmp $9408 ; Original square 2 sfx from z2 .reloc @@ -1775,7 +1819,7 @@ InyIfDownstabbing: .reloc ; Stomp physics between SMB1 and SMB3 are quite different, so add extra bounce force -; to make it feel better. i just like it mmmkay +; to make it feel better. the statue is "heavier" so you bounce less. ApplyStatueBounceForce: lda StatueTimer beq @done diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s index 40906fdf0..00baa2267 100644 --- a/RandomizerCore/Asm/z2mario/sfx.s +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -2,9 +2,55 @@ .include "z2r.inc" .export Square1SfxHandler, Square2SfxHandler, NoiseSfxHandler +.export ResetSquare1Sfx, ResetSquare2Sfx, ResetMarioSfxState +.export ReleaseSquare1Sfx, ReleaseSquare2Sfx .segment "PRG6" +; These are all additions made to the vanilla smb1 sfx engine to make it play better with +; the z2 sound engine. `Release` funcions clear just the buffer state without touching registers +; while `Reset` also touches the registers to clear the sweep state. + +; The only way we can disable sweep from smb1 with how the engine is setup is through +; setting the negate flag here +SWEEP_OFF = $08 + +ReleaseSquare1Sfx: + lda #$00 + sta Square1SoundBuffer + sta Squ1_SfxLenCounter + rts + +ReleaseSquare2Sfx: + lda #$00 + sta Square2SoundBuffer + sta Squ2_SfxLenCounter + sta Sfx_SecondaryCounter + rts + +ResetSquare1Sfx: + jsr ReleaseSquare1Sfx + lda #SWEEP_OFF + sta SND_SQUARE1_REG+1 + rts + +ResetSquare2Sfx: + jsr ReleaseSquare2Sfx + lda #SWEEP_OFF + sta SND_SQUARE2_REG+1 + rts + +ResetMarioSfxState: + jsr ResetSquare1Sfx + jsr ResetSquare2Sfx + lda #$00 + sta NoiseSoundBuffer + sta Noise_SfxLenCounter + sta Square1SoundQueue + sta Square2SoundQueue + sta NoiseSoundQueue + rts + ;------------------------------------------------------------------------------------- .reloc ; .proc SFXSoundEngine @@ -180,9 +226,8 @@ ContinueTailWag: @streamEnd: lda #$00 sta SND_SQUARE1_REG+2 - sta Squ1_SfxLenCounter - sta Square1SoundBuffer - rts + ; jroweboy: full clear the sfx + jmp StopSquare1Sfx ; Ported from SMB3 in PRG31 $E7C1 TailWag_PokeNote: @@ -255,9 +300,7 @@ ContinueStatuePoof: @streamEnd: lda #$00 sta SND_SQUARE2_REG+2 - sta Squ2_SfxLenCounter - sta Square2SoundBuffer - rts + jmp EmptySfx2Buffer ; jroweboy full clear the sfx ; Square 2 twin of TailWag_PokeNote (SMB3 PRG31 $E7C1) Poof_PokeNote: @@ -440,10 +483,11 @@ DecrementSfx1Length: StopSquare1Sfx: ldx #$00 ;if end of sfx reached, clear buffer stx Square1SoundBuffer ;and stop making the sfx - ; ldx #$0e - ; stx SND_MASTERCTRL_REG - ; ldx #$0f - ; stx SND_MASTERCTRL_REG + ; vanilla stopped sfx through $4015 but we can't be sure we can do that, so + ; jroweboy: added this bit to fully clear the sfx when it ends + stx Squ1_SfxLenCounter + ldx #SWEEP_OFF + stx SND_SQUARE1_REG+1 ExSfx1: rts PlayPipeDownInj: @@ -543,12 +587,14 @@ DecrementSfx2Length: EmptySfx2Buffer: ldx #$00 ;initialize square 2's sound effects buffer stx Square2SoundBuffer + stx Squ2_SfxLenCounter StopSquare2Sfx: - ; ldx #$0d ;stop playing the sfx - ; stx SND_MASTERCTRL_REG - ; ldx #$0f - ; stx SND_MASTERCTRL_REG + ; vanilla stopped sfx through $4015 but we can't be sure we can do that, so + ; just clear the sweep + ; jroweboy: fully clear the sfx state when a sfx ends + ldx #SWEEP_OFF + stx SND_SQUARE2_REG+1 ExSfx2: rts Square2SfxHandler: From 55a709f48aa3b2a9b9e791df1dffe1bc7a917bd4 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 20 Jul 2026 18:17:19 -0400 Subject: [PATCH 115/128] Add pow shockwave. Probably need to check if enemy is onscreen eventually. --- RandomizerCore/Asm/z2mario/integration.s | 50 ++++++++++++++++++++---- RandomizerCore/Asm/z2mario/mario.s | 4 +- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index 53198e300..ef8a65e17 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -904,6 +904,45 @@ SetupMarioControl: .org $9041 .word (GameRoutines) +.reloc +.export PowShockwaveCheck +.proc PowShockwaveCheck + lda HaveStabs + and #HAVE_UPSTAB + beq @skip + ; We have upstab and we just broke a block through a head bump + ; So trigger a screen wide thunder-style effect that hits all enemies + ldx #1 + stx $0B + ldx #5 + @loop: + lda Enemy0Status,x + cmp #1 + bne @next + ldy EnemyType,x + lda $6E41,y + and #$A0 + bmi @next + ; Killing enemies reloads X from $10 so we need update it. + stx $10 + jsr $E723 ; damage code but with the current attack level + + @next: + dex + bpl @loop + ldx $10 + beq @nothingHit + ; something was hit so use the pow (blast) sfx instead + lda #Sfx_Blast + sta Square2SoundQueue + ldx #0 + stx Z2NoiseSoundQueue +@nothingHit: + ; restore $10 just in case we need that + stx $10 +@skip: + rts +.endproc .segment "PRG7" ; Hook the link hit routine to check if we are doing star power. If we are @@ -915,13 +954,13 @@ SetupMarioControl: PatchLinkHitDispatch: lda StarInvincibleTimer beq @notStar - lda EnemyState,x - and #~$10 - sta EnemyState,x ; The custom boss has its own invincibility window that 50 damage a frame ; would blow straight through, so star only blocks its hits. lda boss_animation bne @skiphit + lda EnemyState,x + and #~$10 + sta EnemyState,x ; check if the enemy is currently already hit by us lda Enemy0HitState,x bne @skiphit @@ -943,14 +982,9 @@ PatchLinkHitDispatch: pla ; Run the deal damage routine with the "projectile attack" flag ($0B) set, ; otherwise a surviving enemy recoils mario like a melee sword hit would. - ; $0B doubles as mario's Up_Down_Buttons, so save and restore it. - lda $0B - pha lda #1 sta $0B jsr $E726 - pla - sta $0B rts @notStar: lda StatueTimer diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s index 403a123a9..bcee7b01d 100644 --- a/RandomizerCore/Asm/z2mario/mario.s +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -2161,7 +2161,9 @@ PlayerHeadCollision: ldy $2 sta ($c),y sta Player_Y_Speed ;init player's vertical speed - jmp bank7_stab_brick_at_0E_with_A_and_does_draw + jsr bank7_stab_brick_at_0E_with_A_and_does_draw +.import PowShockwaveCheck + jmp PowShockwaveCheck ExitFireballNearby: rts ;leave! ExitBackToBump: From 4531f1d93aa7b491f515bd2e625d203769bd3739 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 20 Jul 2026 18:26:14 -0400 Subject: [PATCH 116/128] Fix crash from growing in swamp --- RandomizerCore/Asm/z2mario/integration.s | 2 +- RandomizerCore/Asm/z2mario/metasprite.s | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index ef8a65e17..f0cad5f45 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -361,7 +361,7 @@ PatchLinkDrawRoutine: @skipplayer: ; While we are here, lets draw the fireball/hammer sprites too ldy #ProjectileOffset - sty $02 + sty R2 @fireballLoop: ldx ObjectMetasprite,y beq @nofireball diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s index 076df770a..8c0fd9c7c 100644 --- a/RandomizerCore/Asm/z2mario/metasprite.s +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -428,6 +428,11 @@ SetYOffset: ora SprObject_SprAttrib,y sta Atr + ; Remember the OAM offset before drawing so we can tell exactly how many + ; sprites this metasprite actually wrote. Needed for swamp handling later. + lda CurrentOAMOffset + sta $0b ; $0b is unused in the metasprite rendering so its safe here + txa pha tya @@ -470,9 +475,13 @@ SetYOffset: ; stuck in the mud/swamp, so update bottom two sprite ; x = next oam sprite id, so we can offset by 8 to hit bottom two sprites ldx CurrentOAMOffset - ; if we are small then its the most recent two sprites. if we are big then its 4 sprites back - lda PlayerSize - beq + ; If we are large + ; if we draw more than 16 sprites then we need to hide the bottom two rows + ; else we hide the previous two. + txa + sec + sbc $0b + cmp #16 + bcs @bigmario lda Sprite_Attributes - 8, x ora #$20 sta Sprite_Attributes - 8, x @@ -480,7 +489,7 @@ SetYOffset: ora #$20 sta Sprite_Attributes - 4, x rts - + + @bigmario: lda Sprite_Attributes - 16, x ora #$20 sta Sprite_Attributes - 16, x From 4bfa6c410ba9400beb7680fcb09df20afd31783c Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 22 Jul 2026 01:49:06 -0400 Subject: [PATCH 117/128] Add a ton of custom text options for walking npcs --- RandomizerCore/CustomTexts.cs | 149 +++++++++++++++++++++++++++++++--- RandomizerCore/Hyrule.cs | 65 +++++++++++++++ RandomizerCore/Music.cs | 6 +- 3 files changed, 208 insertions(+), 12 deletions(-) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 9e8d55543..bd9ee2f6c 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -50,6 +50,8 @@ public class CustomTexts private static readonly int[] newkasutoHints = [83 /* greeting lady at town entrance */, 68 /* text on wall inside house on 1st screen */, /*92 unreachable Lady in Magic Container house (removed) */]; private static readonly int[] oldkasutoHint = [74 /* readable wall inside last house on 2nd screen */]; + private static readonly int[] genericWestMoving = [4, 5, 6, 7, 8]; + private static readonly int[] genericEastMoving = [56, 57, 58, 59]; private static readonly int[] rauruMoving = [9, 10]; private static readonly int[] rutoMoving = [17, 19]; private static readonly int[] sariaMoving = [27]; @@ -91,6 +93,10 @@ public class CustomTexts public const int WEST_TEXT_COUNT = 52; private const int MAX_TEXT_LENGTH = 3134; private const int numberOfTextEntries = 98; + + // On the off chance we do something that eats up space in PRG3, we can limit + // the silly dialog here so that it doesn't eat up extra space for no reason. + private const int MOVING_NPC_DIALOG_MAX_BYTES = 3000; //TODO: This should just be a listing of every text index by continent, but for now it's a pile. private const int westAlreadyGotItemTextIndex = 16; private const int eastAlreadyGotItemTextIndex = 71; @@ -309,6 +315,77 @@ public class CustomTexts "If I knew$something,$I would$tell you" ]; + public static readonly string[] MARIO_KNOW_NOTHING_TEXTS = + [ + "Link, you$are looking$a little$red today.$Are you$well?", + "Wow$you really$took the$wrong pipe", + "You are$gonna save$Hyrule?$You dont$even have$a sword!", + "With those$ears you$must be a$Gerudo.", + "Are you$okay...$you look a$little red", + "Mamma$mia", + "Your$princess$is in$another$castle!", + "I am sorry$but we do$not sell$mushrooms", + "Bowser$sends$his$regards.", + "You look$better in$green", + "Do you$have a$cuter$younger$brother?", + "Link was$looking for$you...$Did you$steal his$crystals?", + "Did you$come here$by pipe or$by kart?", + "Where is$Luigi$", + "Where is$your sword", + "Thank you$but your$princess$is in a$different$game", + "Of course$I know you.$Everyone$knows$red Luigi!", + "Jumping$without$the magic$words...$Thats not$fair.", + "Your coins$are no$good here.", + "Did you$get a$concussion$by hitting$your head$so often?", + "What are$you?$a cop?", + "The river$devil does$not like$plumbers.", + "Get the$power.$Nintendo$power.", + "I dont$talk with$plumbers.", + "Where in$the world$is link?$He smashed$my pots$and left!", + "Ganon is$so scary$please$save us!", + "I$I said$I said this$I said this$to waste$your time.", + "Welcome$stranger!$Hyrule is$a great$place to$live.", + "I am$surprised$that we$speak the$same$language", + "...", + "Well$excuuuuuuse$me, Mario!", + "Where did$you learn$to jump$like that?", + "$$$$$... hi", + "Save our$town and$i will let$you break$all our$pots!", + "Are you$gonna save$us or what?", + "So you$saved us.$$Big deal.", + "Stop by$my shop$sometime.$Oh, you$dont have$any rupees.", + "We welcome$all heroes.", + "Take a$minute to$enjoy the$scenery", + "Hope you$have a fun$stay in$our town!", + "Why dont$you have$an outline?", + "How can$your neck$support$that big$head?", + "Try out$the Zelda 2$randomizer!", + "Try out$other hacks$by jroweboy", + "Go save$your own$princess,$bub.", + "Wish I$could get$as strong$as you", + "Isnt it$cheating$to start$with$downstab?", + "Link had$to work$to earn$those$crystals$you know...", + "Why is it$a triforce$if there$is only one$triangle?", + "I am a$big fan of$jump man.", + "What ever$happened$to your$famous$hotel chain", + "Woah, not$everyday$you see$a man with$your build.", + "Gotta say,$Zelda is a$cooler name$than Peach.", + "Welcome!", + "Hello$there!", + "Please$stay as$long as you$need!", + "I am in$a hurry. No$time to$explain!", + "Time lost.$Reset your$speedrun.", + "I always$feel like$ganon is$watching me", + "The eyes$of Bowser$are$nowhere.", + "The eyes$of Ganon$are$everywhere.", + "So long,$I am off$to save$hyrule!", + "Oh, didnt$see you$there!", + "I am much$too busy$to talk$right now.", + "I heard$Kasuto is$not doing$so well.", + "Our towns$are named$after the$heroes of$old", + "Is there$even a king$in the$mushroom$kingdom?", + ]; + public static readonly string[] KNOW_NOTHING_OUTSIDE_TEXTS = [ .. KNOW_NOTHING_TEXTS, @@ -600,10 +677,11 @@ public static List GenerateTexts( } } + + List placedIndexes = props.HelpfulHints ? GenerateHelpfulHints(texts, itemLocations, hashRNG, props) : []; if (props.HelpfulHints || props.MarioMode) { - List placedIndexes = GenerateHelpfulHints(texts, itemLocations, hashRNG, props); - GenerateKnowNothings(texts, placedIndexes, nonhashRNG, props.BagusWoods, props.UseCommunityText); + GenerateKnowNothings(texts, placedIndexes, nonhashRNG, props.UseCommunityText, props.MarioMode); } if (props.UseCommunityText) @@ -727,9 +805,9 @@ private static Text GenerateWizardText(List texts, Random r, Location loca case Collectable.JUMP_SPELL: return new Text("JUMP WONT$HELP YOU.$TRY THIS$INSTEAD."); case Collectable.DOWNSTAB: - return new Text("HERE IS$HOW TO$PIERCE$SHIELDS."); + return new Text("PIERCE$SHIELDS$WITH YOUR$STOMP."); case Collectable.UPSTAB: - return new Text("UP B TO$THROW$HAMMER$WHEN SMALL"); + return new Text("BREAK BLOCKS$WITH HEAD$TO CREATE$POW-ERFUL$SHOCKWAVE"); case Collectable.FIRE_SPELL: return new Text("THIS MAGIC$WILL MAKE$YOUR HAND$THROW FIRE"); case Collectable.SPELL_SPELL: @@ -786,13 +864,24 @@ private static Text GenerateRiverManText(Random r) return new Text(RIVER_MAN_TEXTS[r.Next(RIVER_MAN_TEXTS.Length)]); } - private static void GenerateKnowNothings(List hints, List placedIndexes, Random r, bool useBaguWoods, bool useCommunityText) + private static void GenerateKnowNothings(List hints, List placedIndexes, Random r, bool useCommunityText, bool marioMode) { - ShuffleBag kidKnowNothingTexts = new(KNOW_NOTHING_KID_TEXTS, r); - ShuffleBag sariaGreeterTexts = new(KNOW_NOTHING_SARIA_GREETER_TEXTS, r); - ShuffleBag outsideKnowNothingTexts = new(KNOW_NOTHING_OUTSIDE_TEXTS, r); - ShuffleBag knowNothingTexts = new(KNOW_NOTHING_TEXTS, r); - ShuffleBag movingNpcTexts = new(MOVING_NPC_TEXTS, r); + string[][] allTexts = [ + KNOW_NOTHING_KID_TEXTS, + KNOW_NOTHING_SARIA_GREETER_TEXTS, + KNOW_NOTHING_OUTSIDE_TEXTS, + KNOW_NOTHING_TEXTS, + MOVING_NPC_TEXTS + ]; + var maybeMarioAdded = (marioMode) ? + allTexts.Select(list => (string[])[..MARIO_KNOW_NOTHING_TEXTS, ..list]).ToArray() + : allTexts; + ShuffleBag marioOnlyTexts = new(MARIO_KNOW_NOTHING_TEXTS, r); + ShuffleBag kidKnowNothingTexts = new(maybeMarioAdded[0], r); + ShuffleBag sariaGreeterTexts = new(maybeMarioAdded[1], r); + ShuffleBag outsideKnowNothingTexts = new(maybeMarioAdded[2], r); + ShuffleBag knowNothingTexts = new(maybeMarioAdded[3], r); + ShuffleBag movingNpcTexts = new(maybeMarioAdded[4], r); List stationary = [ @@ -812,6 +901,8 @@ private static void GenerateKnowNothings(List hints, List placedIndex List moving = [ + .. genericEastMoving, + .. genericWestMoving, .. rauruMoving, .. rutoMoving, .. sariaMoving, @@ -847,6 +938,10 @@ private static void GenerateKnowNothings(List hints, List placedIndex hint = new Text(knowNothingTexts.Draw()); } } + else if (marioMode) + { + hint = new Text(marioOnlyTexts.Draw()); + } else { hint = defaultKnowNothing; @@ -865,7 +960,7 @@ private static void GenerateKnowNothings(List hints, List placedIndex for (int i = 0; i < moving.Count; i++) { - hints[moving[i]] = useCommunityText ? new Text(movingNpcTexts.Draw()) : defaultKnowNothing; + hints[moving[i]] = useCommunityText ? new Text(movingNpcTexts.Draw()) : marioMode ? new Text(marioOnlyTexts.Draw()) : defaultKnowNothing; } } @@ -1096,6 +1191,38 @@ private static int TextLength(List texts) return sum; } + public static List BuildMovingNpcDialogPool(RandomizerProperties props) + { + List raw = []; + if (props.UseCommunityText) + { + raw.AddRange(MOVING_NPC_TEXTS); + raw.AddRange(KNOW_NOTHING_TEXTS); + } + if (props.MarioMode) + { + raw.AddRange(MARIO_KNOW_NOTHING_TEXTS); + } + + // Dedup by string (preserving first-seen order) so no line is stored twice in ROM. + List pool = [.. raw.Distinct().Select(s => new Text(s))]; + // Trim the list of texts back until it fits in the max size given + while (pool.Count > 1 && MovingNpcDialogByteLength(pool) > MOVING_NPC_DIALOG_MAX_BYTES) + { + pool.RemoveAt(pool.Count - 1); + } + + return pool; + } + + private static int MovingNpcDialogByteLength(List pool) + { + const int rollRoutineBytes = 24; + int stringBytes = pool.Sum(t => t.EncodedText.Length); + int pointerTableBytes = pool.Count * 2; + return stringBytes + pointerTableBytes + rollRoutineBytes; + } + /// Make sure all strings are max 11x6 [Conditional("DEBUG")] private static void DebugValidateTexts() diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index f96f8dbb4..bc6cf5175 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3698,6 +3698,70 @@ private static void UpdateTexts(Assembler asm, List hints) } } + private static void UpdateMovingNpcDialog(Assembler asm, List texts) + { + if (texts.Count == 0) + { + return; + } + var a = asm.Module(); + a.Assign("MOVING_DIALOG_COUNT", texts.Count); + a.Segment("PRG3"); + a.Reloc(); + a.Label("MovingDialogTable"); + for (var i = 0; i < texts.Count; i++) + { + a.Word(a.Symbol($"MovingDialogText{i}")); + } + for (var i = 0; i < texts.Count; i++) + { + a.Reloc(); + a.Label($"MovingDialogText{i}"); + a.Byt(texts[i].EncodedText); + } + + a.Code(""" +.include "z2r.inc" +LoadTextFromPointer = $b609 + +; patch the random walking NPC dialog to choose a dialog on spawn +.org $973b + jsr ChooseDialogOnSpawn +.reloc +ChooseDialogOnSpawn: + lda RNG,x + ; RNG modulo COUNT of texts + cmp #MOVING_DIALOG_COUNT + bcc + + ; subtract until we underflow +- sbc #MOVING_DIALOG_COUNT + bcs - + ; then add it back once to complete the modulo + adc #MOVING_DIALOG_COUNT ++ + sta EnemyHP,x + jmp $976B ; patched code + +; Repoint the walking generic NPC dialogs to our new routine +.org $b46c + .word CustomMovingNpcDialog + .word CustomMovingNpcDialog + .word CustomMovingNpcDialog + .word CustomMovingNpcDialog + +.reloc +CustomMovingNpcDialog: + lda EnemyHP,x ; repurposed as villager dialog option + asl + tay + lda #MovingDialogTable + sta $01 + jmp LoadTextFromPointer +"""); + } + private void AssignRealPalaceLocations(AsmModule a) { a.Assign("RealPalaceAtLocation1", (westHyrule?.locationAtPalace1.PalaceNumber ?? 1) - 1); @@ -4012,6 +4076,7 @@ void PatchAddress(byte opcode, int before, int after) } } + UpdateMovingNpcDialog(engine, CustomTexts.BuildMovingNpcDialogPool(props)); UpdateTexts(engine, texts); } diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index 42a4b9a08..8f1cbf00c 100644 --- a/RandomizerCore/Music.cs +++ b/RandomizerCore/Music.cs @@ -31,6 +31,7 @@ internal enum Usage Credits, GameComplete, House, + PalaceClear, } /*internal class LoggerAdapter : Logger @@ -103,8 +104,9 @@ record BuiltinSongInfo(int Index, string Title, Usage[] Uses); new(7, "House", [Usage.House]), new(9, "Palace", [Usage.Palace]), new(0xb, "Boss", [Usage.Boss]), + new(0xc, "Palace Clear", [Usage.PalaceClear]), new(0xd, "Great Palace", [Usage.GreatPalace]), - new(0xf, "GameComplete", [Usage.GameComplete]), + new(0xf, "Game Complete", [Usage.GameComplete]), new(0x10, "Credits", [Usage.Credits]), new(0x12, "Last Boss", [Usage.LastBoss]), }; @@ -208,6 +210,7 @@ public void ImportSongs() { Usage.Credits, 1 }, { Usage.GameComplete, 1 }, { Usage.House, 1 }, + { Usage.PalaceClear, 1 }, }; var selUsesSongs = _imptr.SelectUsesSongs( usesSongs, numUsageSongs, _shuffler); @@ -331,6 +334,7 @@ SongMap CreateSongMap(UsesSongs usesSongs) { Usage.Overworld, 1 }, { Usage.House, 7 }, { Usage.Boss, 0xb }, + { Usage.PalaceClear, 0xc }, { Usage.LastBoss, 0x12 }, { Usage.Credits, 0x10 }, { Usage.GameComplete, 0xf }, From a91ae9bdbc2633e6afda77740b1324363bc46754 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 22 Jul 2026 15:13:39 -0400 Subject: [PATCH 118/128] Move more stuff outta fixed bank/prg5 into expanded banks --- .../Asm/z2mario/CustomTitleScreen.s | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/CustomTitleScreen.s b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s index dee47d98f..849a58410 100644 --- a/RandomizerCore/Asm/z2mario/CustomTitleScreen.s +++ b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s @@ -65,10 +65,10 @@ FREE "PRG5" [$A7C1, $A932) .segment "PRG5" .org $A72E - jsr InitTitleStars + jsr BankedInitTitleScreen .org $AE41 - jsr InitTitleStars + jsr BankedInitTitleScreen .org $A6F7 jmp *+6 @@ -282,8 +282,18 @@ TitleTimerTargetTimerHi: ; Well thats a problem when i want to use mario on the screen, and stars shouldn't twinkle brown. ; So instead, we lose one color from the star twinkle, (the deep $02 blue from before), and instead ; put all stars on the same palette index 0. We then use two different "star sprites" that use color -; 1 and 2 as well as the vanilla color 3. That way the stars can twinkle independently of each other. -.segment "PRG5" +; 1 and 2 as well as the vanilla color 3. That way the stars can twinkle independently of each other. +.segment "PRG7" +.reloc +BankedInitTitleScreen: + lda #$e ; use the same bank the extended rooms use + sta CurrentPrgBank + jsr SwapPRG + jsr InitTitleStars + lda #5 + sta CurrentPrgBank + jmp SwapPRG +.segment "PRG1C", "PRG1D" .reloc InitTitleStars: lda Respawning @@ -416,7 +426,6 @@ BarebonesTitleScreenRNG: ;; --- ; Drawing routines -.segment "PRG7" .reloc InitMarioData: ldx #0 @@ -441,6 +450,7 @@ InitMarioData: sta Player_Y_Position rts +.segment "PRG7" .reloc DrawTitleMario: jsr SwapToPRG0 From 2490988c7a6b03fe077017c0fe66aa7911f43927 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 22 Jul 2026 17:16:52 -0400 Subject: [PATCH 119/128] Fix hammer hitting statues and add coin sfx on file load --- RandomizerCore/Asm/Init.s | 4 +- RandomizerCore/Asm/StatTracking.s | 9 +++ RandomizerCore/Asm/z2mario/integration.s | 74 +++++++++++++++--------- 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/RandomizerCore/Asm/Init.s b/RandomizerCore/Asm/Init.s index 79bc84ebe..6704450aa 100644 --- a/RandomizerCore/Asm/Init.s +++ b/RandomizerCore/Asm/Init.s @@ -58,7 +58,9 @@ FREE "PRG4" [$870E, $871B) FREE "PRG4" [$8817, $88A0) FREE "PRG4" [$8EC3, $9400) FREE "PRG4" [$9EE0, $a000) -FREE "PRG4" [$A1E3, $A1F8) +; not strictly unused. its copied to ram for palace data, +; it may really be unused but don't risk it. +; FREE "PRG4" [$A1E3, $A1F8) FREE "PRG4" [$A3FB, $A440) FREE "PRG4" [$A539, $A640) FREE "PRG4" [$A765, $A900) diff --git a/RandomizerCore/Asm/StatTracking.s b/RandomizerCore/Asm/StatTracking.s index acefe1610..bd4a37a96 100644 --- a/RandomizerCore/Asm/StatTracking.s +++ b/RandomizerCore/Asm/StatTracking.s @@ -412,6 +412,15 @@ CheckIfNewSaveFile: bne @loop sta StatTrackingSaveFileClear @skip: +.ifdef ENABLE_Z2_MARIO + ; also play a coin sfx when entering the file + pha + lda #0 + sta Square1SoundQueue + lda #Sfx_CoinGrab + sta Square2SoundQueue + pla +.endif jmp LoadSaveFile ; Patch the "Register" button to set a flag for a new save file diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s index f0cad5f45..da3c1d9a8 100644 --- a/RandomizerCore/Asm/z2mario/integration.s +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -1356,6 +1356,7 @@ CheckHammerHitboxes: .reloc PatchFireballHitcheck: ldy $11 ; fireball or hammer slot (0 or 1) +PatchFireballHitcheckInternal: lda Fireball_State,y and #%01000000 bne @IsHammer @@ -1396,35 +1397,56 @@ PatchFireballHitcheck: ; the patch check above FREE_UNTIL $e6f3 -; .org $E558 -; rts ; disable link shield maybe? -; .org $E563 -; rts ; disable link shield maybe? -; .org $E579 -; rts ; disable link shield maybe? - -; Replace vanilla beam/fire code with our new +; Skip over something? this was related to projectiles... .org $d50d - ; jsr CustomProjectileCode jmp *+3 -; .reloc -; CustomProjectileCode: -; jsr ProcFireball_Bubble ;process fireballs and air bubbles -; rts -; .org $9370 -; ; TODO this is where link takes damage -; jmp GameRoutines - -; .org $937e -; nop ; don't do this sword attack check -; nop ; todo see what should be here instead -; nop -; ; there's a branch here from above to call the main game routine -; jmp GameRoutines - -; .org $93ba -; rts ; patch the link main routine to skip some unneeded stuff +; Make it so hammers can hit statues/jars and reveal them +; this is a macro since we need this in PRG4 and PRG5. we could put it in PRG7 +; but I don't want to waste room there +; Check hammers only every other frame too. +.macro CheckHammerHitboxMacro + jsr $E677 ; Check collision + bcs @exit +@check1: + ; Didn't hit with the vanilla sword check, so check hammers too + lda LinkProjectile0State + beq @check2 + lda FrameCounter + and #1 ; cant use lsr to keep the carry + bne @check2 + ldy #0 + jsr LoadProjectileCollisionBox + jsr PatchFireballHitcheckInternal + bcs @exit +@check2: + lda LinkProjectile1State + beq @exit + lda FrameCounter + and #1 ; cant use lsr to keep the carry + beq @exit + ldy #1 + jsr LoadProjectileCollisionBox + jmp PatchFireballHitcheckInternal +@exit: + rts +.endmacro + +.segment "PRG4" +.org $AF65 + jsr CheckStatueHammerPRG4 +.org $B842 + jsr CheckStatueHammerPRG4 +.reloc +CheckStatueHammerPRG4: + CheckHammerHitboxMacro + +.segment "PRG5" +.org $A525 + jsr CheckStatueHammerPRG5 +.reloc +CheckStatueHammerPRG5: + CheckHammerHitboxMacro ; Update the falling block "set block on ground" so that it updates marios From 257d1854a62f96ecb00cca6405bcced92729d3e9 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 22 Jul 2026 18:49:16 -0400 Subject: [PATCH 120/128] Add new z2m credits --- RandomizerCore/Hyrule.cs | 68 ++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index bc6cf5175..f84341943 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3819,7 +3819,7 @@ public void StatTracking(RandomizerProperties props, Assembler asm) a.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.StatTracking.s"), "stat_tracking.s"); } - public void AddCredits(Assembler asm) + public void AddCredits(Assembler asm, RandomizerProperties props) { byte[] CmdText(int x, int y, string text) { @@ -3847,18 +3847,66 @@ .. CmdText(5, 7, "JROWEBOY"), .. CmdText(21, 7, "INITSU"), a.Label("NewCreditsBody"); a.Byt(body); - a.Code(""" + if (props.MarioMode) + { + // New Zelda II Mario credit screens which comes after the z2 credits but before the + // rando credits + byte[] marioRole1 = [.. CmdText(7, 2, "Z2M PROGRAMER"), 0xff]; + byte[] marioName1 = [.. CmdText(11, 4, "JROWEBOY"), 0xff]; + byte[] marioRole2 = [.. CmdText(7, 2, "Z2M SOUND COMPOSER"), 0xff]; + byte[] marioName2 = [.. CmdText(11, 4, "HEYDON "), 0xff]; + byte[] marioRole3 = [.. CmdText(7, 2, "SPECIAL THANKS "), 0xff]; + byte[] marioName3 = [.. CmdText(11, 4, "STUDSX"), .. CmdText(11, 6, "QUANTAM"), 0xff]; + byte[] marioRole4 = [.. CmdText(7, 2, "SPECIAL THANKS"), 0xff]; + byte[] marioName4 = [.. CmdText(11, 4, "YOU "), .. CmdText(11, 6, " "), 0xff]; + a.Assign("ENABLE_Z2_MARIO", 1); + + a.Reloc(); a.Label("MarioRole1"); a.Byt(marioRole1); + a.Reloc(); a.Label("MarioName1"); a.Byt(marioName1); + a.Reloc(); a.Label("MarioRole2"); a.Byt(marioRole2); + a.Reloc(); a.Label("MarioName2"); a.Byt(marioName2); + a.Reloc(); a.Label("MarioRole3"); a.Byt(marioRole3); + a.Reloc(); a.Label("MarioName3"); a.Byt(marioName3); + a.Reloc(); a.Label("MarioRole4"); a.Byt(marioRole4); + a.Reloc(); a.Label("MarioName4"); a.Byt(marioName4); + } + + ushort[] vanillaEntries = [ + 0x927D, 0x9293, 0x92A1, 0x92B7, 0x92C5, 0x92D1, 0x92E6, 0x92F2, + 0x9307, 0x9319, 0x9325, 0x9337, 0x9325, 0x937E, 0x9325, 0x9396]; + a.Reloc(); + a.Label("NewCreditTable"); + a.Word(vanillaEntries); + if (props.MarioMode) + { + a.Word(a.Symbol("MarioRole1")); a.Word(a.Symbol("MarioName1")); + a.Word(a.Symbol("MarioRole2")); a.Word(a.Symbol("MarioName2")); + a.Word(a.Symbol("MarioRole3")); a.Word(a.Symbol("MarioName3")); + a.Word(a.Symbol("MarioRole4")); a.Word(a.Symbol("MarioName4")); + } + a.Word(a.Symbol("NewCreditsHeader")); + a.Word(a.Symbol("NewCreditsBody")); + + a.Code($$""" .include "z2r.inc" .segment "PRG5" -bank5_Pointer_table_for_End_Credits: -.org $9259 ; first entry -.org $9279 - .word NewCreditsHeader -.org $927b - .word NewCreditsBody - +.org $9095 + lda NewCreditTable,y +.org $909a + lda NewCreditTable+1,y + +.ifdef ENABLE_Z2_MARIO +.org $9175 + cmp #$12 + 8 ; we added 8 new entries to the credits +.org $9196 + cmp #$10 + 8 +.endif + +; Free the now unused table and stuff +.org $9259 + FREE_UNTIL $927D .org $934F FREE_UNTIL $9364 .org $9364 @@ -3906,7 +3954,7 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando rom.FixBossKillPaletteGlitch(engine); rom.FixBigBubbleSplit(engine, randomizedStats); StatTracking(props, engine); - AddCredits(engine); + AddCredits(engine, props); if (props.MarioMode) { From eb353ab9362aa682316c8571ba944f93dba88e55 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 22 Jul 2026 21:14:01 -0400 Subject: [PATCH 121/128] Fix up dialog support for mario mode --- RandomizerCore/Asm/Init.s | 6 ++-- RandomizerCore/CustomTexts.cs | 68 ++++++++++++++++++----------------- RandomizerCore/Hyrule.cs | 11 ++++++ 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/RandomizerCore/Asm/Init.s b/RandomizerCore/Asm/Init.s index 6704450aa..2074c6088 100644 --- a/RandomizerCore/Asm/Init.s +++ b/RandomizerCore/Asm/Init.s @@ -46,9 +46,9 @@ FREE "PRG2" [$93c9, $9400) FREE "PRG2" [$9f85, $a000) FREE "PRG2" [$a933, $b480) -;FREE "PRG3" [$8bb1, $90f0] -;FREE "PRG3" [$9134, $9400] -;FREE "PRG3" [$9d0a, $a000] +FREE "PRG3" [$8bb1, $90f0] +FREE "PRG3" [$9134, $9400] +FREE "PRG3" [$9d0a, $a000] FREE "PRG3" [$B803, $c000) FREE "PRG4" [$83DC, $8470) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index bd9ee2f6c..f6f40f1c9 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -681,7 +681,7 @@ public static List GenerateTexts( List placedIndexes = props.HelpfulHints ? GenerateHelpfulHints(texts, itemLocations, hashRNG, props) : []; if (props.HelpfulHints || props.MarioMode) { - GenerateKnowNothings(texts, placedIndexes, nonhashRNG, props.UseCommunityText, props.MarioMode); + GenerateKnowNothings(texts, placedIndexes, nonhashRNG, props.HelpfulHints, props.UseCommunityText, props.MarioMode); } if (props.UseCommunityText) @@ -864,7 +864,7 @@ private static Text GenerateRiverManText(Random r) return new Text(RIVER_MAN_TEXTS[r.Next(RIVER_MAN_TEXTS.Length)]); } - private static void GenerateKnowNothings(List hints, List placedIndexes, Random r, bool useCommunityText, bool marioMode) + private static void GenerateKnowNothings(List hints, List placedIndexes, Random r, bool helpfulHints, bool useCommunityText, bool marioMode) { string[][] allTexts = [ KNOW_NOTHING_KID_TEXTS, @@ -873,7 +873,7 @@ private static void GenerateKnowNothings(List hints, List placedIndex KNOW_NOTHING_TEXTS, MOVING_NPC_TEXTS ]; - var maybeMarioAdded = (marioMode) ? + var maybeMarioAdded = marioMode ? allTexts.Select(list => (string[])[..MARIO_KNOW_NOTHING_TEXTS, ..list]).ToArray() : allTexts; ShuffleBag marioOnlyTexts = new(MARIO_KNOW_NOTHING_TEXTS, r); @@ -913,48 +913,50 @@ private static void GenerateKnowNothings(List hints, List placedIndex ]; Text defaultKnowNothing = new(); - for (int i = 0; i < stationary.Count; i++) + + // if we are doing mario mode, we could have helpful hints off but still want mario flavor text + // for the moving NPCs, so skip stationary onces + if (helpfulHints) { - int textIndex = stationary[i]; - if (!placedIndexes.Contains(textIndex)) + for (int i = 0; i < stationary.Count; i++) { - Text hint; - if (useCommunityText) + int textIndex = stationary[i]; + if (!placedIndexes.Contains(textIndex)) { - if (kidHintNpcs.Contains(textIndex)) + Text hint; + if (useCommunityText) { - hint = new Text(kidKnowNothingTexts.Draw()); + if (kidHintNpcs.Contains(textIndex)) + { + hint = new Text(kidKnowNothingTexts.Draw()); + } + else if (textIndex == SariaGreeterNpc) + { + hint = new Text(sariaGreeterTexts.Draw()); + } + else if (outsideHintNpcs.Contains(textIndex)) + { + hint = new Text(outsideKnowNothingTexts.Draw()); + } + else + { + hint = new Text(knowNothingTexts.Draw()); + } } - else if (textIndex == SariaGreeterNpc) + else { - hint = new Text(sariaGreeterTexts.Draw()); + hint = defaultKnowNothing; } - else if (outsideHintNpcs.Contains(textIndex)) + hints[textIndex] = hint; + if (textIndex == talkingAcheIndexTalking) { - hint = new Text(outsideKnowNothingTexts.Draw()); + hints[talkingAcheIndexSleeping] = hint; } - else + else if (textIndex == talkingBotIndexTalking) { - hint = new Text(knowNothingTexts.Draw()); + hints[talkingBotIndexSleeping] = hint; } } - else if (marioMode) - { - hint = new Text(marioOnlyTexts.Draw()); - } - else - { - hint = defaultKnowNothing; - } - hints[textIndex] = hint; - if (textIndex == talkingAcheIndexTalking) - { - hints[talkingAcheIndexSleeping] = hint; - } - else if (textIndex == talkingBotIndexTalking) - { - hints[talkingBotIndexSleeping] = hint; - } } } diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index f84341943..d773d94ae 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -3724,9 +3724,16 @@ private static void UpdateMovingNpcDialog(Assembler asm, List texts) .include "z2r.inc" LoadTextFromPointer = $b609 +.segment "PRG3" + ; patch the random walking NPC dialog to choose a dialog on spawn .org $973b jsr ChooseDialogOnSpawn +; Conveniently the other type of walking NPC spawn +; will also jump to $976B at then end +.org $9753 + jmp ChooseDialogOnSpawn + .reloc ChooseDialogOnSpawn: lda RNG,x @@ -3748,6 +3755,10 @@ .word CustomMovingNpcDialog .word CustomMovingNpcDialog .word CustomMovingNpcDialog .word CustomMovingNpcDialog + .word CustomMovingNpcDialog + .word CustomMovingNpcDialog + .word CustomMovingNpcDialog + .word CustomMovingNpcDialog .reloc CustomMovingNpcDialog: From c5b5a794e384dce0ff9c23462d8304cf8f8647ab Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 22 Jul 2026 22:34:26 -0400 Subject: [PATCH 122/128] I will PHYSICALLY EXPLODE if i have to fix another sfx bug. --- RandomizerCore/Asm/z2mario/sfx.s | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s index 00baa2267..11acc2e79 100644 --- a/RandomizerCore/Asm/z2mario/sfx.s +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -30,12 +30,16 @@ ReleaseSquare2Sfx: ResetSquare1Sfx: jsr ReleaseSquare1Sfx + lda #$10 + sta SND_SQUARE1_REG lda #SWEEP_OFF sta SND_SQUARE1_REG+1 rts ResetSquare2Sfx: jsr ReleaseSquare2Sfx + lda #$10 + sta SND_SQUARE2_REG lda #SWEEP_OFF sta SND_SQUARE2_REG+1 rts @@ -483,9 +487,9 @@ DecrementSfx1Length: StopSquare1Sfx: ldx #$00 ;if end of sfx reached, clear buffer stx Square1SoundBuffer ;and stop making the sfx - ; vanilla stopped sfx through $4015 but we can't be sure we can do that, so - ; jroweboy: added this bit to fully clear the sfx when it ends stx Squ1_SfxLenCounter + ldx #$10 + stx SND_SQUARE2_REG ldx #SWEEP_OFF stx SND_SQUARE1_REG+1 ExSfx1: rts @@ -590,9 +594,8 @@ EmptySfx2Buffer: stx Squ2_SfxLenCounter StopSquare2Sfx: - ; vanilla stopped sfx through $4015 but we can't be sure we can do that, so - ; just clear the sweep - ; jroweboy: fully clear the sfx state when a sfx ends + ldx #$10 + stx SND_SQUARE2_REG ldx #SWEEP_OFF stx SND_SQUARE2_REG+1 ExSfx2: rts From c7db95203d3baed46c52eef2f1d5371b57bed77b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 22 Jul 2026 22:51:08 -0400 Subject: [PATCH 123/128] It was technically a bug with the assembler and not SFX --- RandomizerCore/Asm/z2mario/sfx.s | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s index 11acc2e79..dd9a093e7 100644 --- a/RandomizerCore/Asm/z2mario/sfx.s +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -603,7 +603,9 @@ ExSfx2: rts Square2SfxHandler: lda Square2SoundBuffer ;special handling for the 1-up sound to keep it and #Sfx_ExtraLife ;from being interrupted by other sounds on square 2 - bne ContinueExtraLife + beq + + jmp ContinueExtraLife + + ldy Square2SoundQueue ;check for sfx in queue beq CheckSfx2Buffer sty Square2SoundBuffer ;if found, put in buffer and check for the following @@ -611,13 +613,20 @@ Square2SfxHandler: jmp PlayStatuePoof ;tanooki statue change (was bowser fall) + lsr Square2SoundQueue - bcs PlayCoinGrab ;coin grab - lsr Square2SoundQueue - bcs PlayGrowPowerUp ;power-up reveal + bcc + + jmp PlayCoinGrab ;coin grab + + lsr Square2SoundQueue - bcs PlayGrowVine ;vine grow + bcc + + jmp PlayGrowPowerUp ;power-up reveal + + lsr Square2SoundQueue - bcs PlayBlast ;fireworks/gunfire + bcc + + jmp PlayGrowVine ;vine grow + + lsr Square2SoundQueue + bcc + + jmp PlayBlast ;fireworks/gunfire + + lsr Square2SoundQueue bcc + jmp PlayTimerTick ;timer tick @@ -625,15 +634,18 @@ Square2SfxHandler: lsr Square2SoundQueue bcs PlayPowerUpGrab ;power-up grab lsr Square2SoundQueue - bcs PlayExtraLife ;1-up - + bcc + + jmp PlayExtraLife ;1-up + + CheckSfx2Buffer: lda Square2SoundBuffer ;check for sfx in buffer beq ExS2H ;if not found, exit sub bpl + jmp ContinueStatuePoof ;tanooki statue change (was bowser fall) + lsr - bcs Cont_CGrab_TTick ;coin grab + bcc + + jmp Cont_CGrab_TTick ;coin grab + + lsr bcs ContinueGrowItems ;power-up reveal lsr From 55141aeb3b5cd46aa297467894838fbf9fd25368 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Wed, 22 Jul 2026 23:22:27 -0400 Subject: [PATCH 124/128] ! --- RandomizerCore/CustomTexts.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index f6f40f1c9..9db016592 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -339,7 +339,7 @@ public class CustomTexts "Did you$get a$concussion$by hitting$your head$so often?", "What are$you?$a cop?", "The river$devil does$not like$plumbers.", - "Get the$power.$Nintendo$power.", + "Get the$power.$Nintendo$power!", "I dont$talk with$plumbers.", "Where in$the world$is link?$He smashed$my pots$and left!", "Ganon is$so scary$please$save us!", From c90258240e695ccf51cdc8ac7a00b47c1a3da25b Mon Sep 17 00:00:00 2001 From: James Rowe Date: Thu, 23 Jul 2026 00:30:29 -0400 Subject: [PATCH 125/128] More jump branches --- RandomizerCore/Asm/z2mario/sfx.s | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s index dd9a093e7..c68ee4fc7 100644 --- a/RandomizerCore/Asm/z2mario/sfx.s +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -613,9 +613,9 @@ Square2SfxHandler: jmp PlayStatuePoof ;tanooki statue change (was bowser fall) + lsr Square2SoundQueue - bcc + + bcc + jmp PlayCoinGrab ;coin grab - + + + lsr Square2SoundQueue bcc + jmp PlayGrowPowerUp ;power-up reveal @@ -623,7 +623,8 @@ Square2SfxHandler: lsr Square2SoundQueue bcc + jmp PlayGrowVine ;vine grow - + lsr Square2SoundQueue + + + lsr Square2SoundQueue bcc + jmp PlayBlast ;fireworks/gunfire + @@ -632,7 +633,9 @@ Square2SfxHandler: jmp PlayTimerTick ;timer tick + lsr Square2SoundQueue - bcs PlayPowerUpGrab ;power-up grab + bcc + + jmp PlayPowerUpGrab ;power-up grab + + lsr Square2SoundQueue bcc + jmp PlayExtraLife ;1-up @@ -647,19 +650,23 @@ CheckSfx2Buffer: jmp Cont_CGrab_TTick ;coin grab + lsr - bcs ContinueGrowItems ;power-up reveal + ; bcs ContinueGrowItems ;power-up reveal lsr - bcs ContinueGrowItems ;vine grow + ; bcs ContinueGrowItems ;vine grow lsr bcc + jmp ContinueBlast ;fireworks/gunfire + lsr - bcs Cont_CGrab_TTick ;timer tick + ; bcs Cont_CGrab_TTick ;timer tick lsr - bcs ContinuePowerUpGrab ;power-up grab + bcc + + jmp ContinuePowerUpGrab ;power-up grab + + lsr - bcs ContinueExtraLife ;1-up + bcc + + jmp ContinueExtraLife ;1-up + + ExS2H: rts Cont_CGrab_TTick: From a159ee89e018419c68433e4c7434edb28ca3ab65 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 27 Jul 2026 00:54:38 -0400 Subject: [PATCH 126/128] Fix player turning into link when bridge is built Player chr bank gets cleared and not set again until the bridge is built so we shouldn't clear it on screen transition --- RandomizerCore/Asm/ram.inc | 2 +- RandomizerCore/CustomTexts.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index b59440edf..88a09650d 100644 --- a/RandomizerCore/Asm/ram.inc +++ b/RandomizerCore/Asm/ram.inc @@ -552,6 +552,7 @@ ObjectOffset = $10 ; RESV PlayerEntranceCtrl ; RESV JoypadOverride ; RESV Vine_Height +RESV PlayerChrBank RESV Player_State ; RESV DisableCollisionDet RESV GameEngineSubroutine @@ -615,7 +616,6 @@ ALL_TIMER_COUNT = StarInvincibleTimer - Timers RESV SpriteShuffleOffset ;RESV CurrentCHRBank -RESV PlayerChrBank RESV ReloadCHRBank RESV ObjectVerticalFlip RESV Local_eb diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 9db016592..79848ddd7 100644 --- a/RandomizerCore/CustomTexts.cs +++ b/RandomizerCore/CustomTexts.cs @@ -947,7 +947,7 @@ private static void GenerateKnowNothings(List hints, List placedIndex { hint = defaultKnowNothing; } - hints[textIndex] = hint; + hints[textIndex] = hint; if (textIndex == talkingAcheIndexTalking) { hints[talkingAcheIndexSleeping] = hint; From 06c048af2cb284aea451cdb6a26a0fd1c0226369 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 27 Jul 2026 01:39:59 -0400 Subject: [PATCH 127/128] Clear out the current chr to fix the title screen on second quest --- RandomizerCore/Asm/StatTracking.s | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RandomizerCore/Asm/StatTracking.s b/RandomizerCore/Asm/StatTracking.s index bd4a37a96..5e5382a8a 100644 --- a/RandomizerCore/Asm/StatTracking.s +++ b/RandomizerCore/Asm/StatTracking.s @@ -560,6 +560,8 @@ RenderPlayerInfo: beq @skip jmp RenderPlayerInfoBg @skip: + ; We need to clear CHR bank at some point so might as well do it now. + sta CurrentCHRBank ; Render the sprites jsr DrawPauseMenuRowSwitchBank From b620b7a6ec42210befe1f94e91dc8e211419ecb0 Mon Sep 17 00:00:00 2001 From: James Rowe Date: Mon, 27 Jul 2026 01:40:18 -0400 Subject: [PATCH 128/128] *boom* (i exploded) --- RandomizerCore/Asm/z2mario/sfx.s | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s index c68ee4fc7..961e014b0 100644 --- a/RandomizerCore/Asm/z2mario/sfx.s +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -489,7 +489,7 @@ StopSquare1Sfx: stx Square1SoundBuffer ;and stop making the sfx stx Squ1_SfxLenCounter ldx #$10 - stx SND_SQUARE2_REG + stx SND_SQUARE1_REG ldx #SWEEP_OFF stx SND_SQUARE1_REG+1 ExSfx1: rts