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/CommandLine/DefaultPlayerOptions.json b/CommandLine/DefaultPlayerOptions.json index f52d70332..a3f33c406 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/Program.cs b/CommandLine/Program.cs index 63c085c7f..3e3db67ee 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; @@ -151,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) { @@ -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/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 94091d448..5f265d989 100644 --- a/CommandLine/Properties/launchSettings.json +++ b/CommandLine/Properties/launchSettings.json @@ -2,7 +2,8 @@ "profiles": { "CommandLine": { "commandName": "Project", - "commandLineArgs": "-c ../../../Sample.json -r ../../../../Zelda2.nes -s 12345678 -o ../../../.." + "commandLineArgs": "-f AAABvvyhAmLAAAAAAAAAEuPi+yAAAAAAAAAAAAAbhA -r ../Zelda2.nes -s mario -o ..", + "workingDirectory": "../build/" } } } \ No newline at end of file diff --git a/CommandLine/Sample.json b/CommandLine/Sample.json index e38e0773e..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", @@ -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 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 3bbe79176..c75d9c6d6 100644 --- a/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj +++ b/CrossPlatformUI.Browser/CrossPlatformUI.Browser.csproj @@ -1,105 +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 60f116f2a..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 - - - + + + 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 + + + \ No newline at end of file diff --git a/CrossPlatformUI/CrossPlatformUI.csproj b/CrossPlatformUI/CrossPlatformUI.csproj index 36406d1c1..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/CrossPlatformUI/Lang/Resources.resx b/CrossPlatformUI/Lang/Resources.resx index a336781cc..1b6f700a7 100644 --- a/CrossPlatformUI/Lang/Resources.resx +++ b/CrossPlatformUI/Lang/Resources.resx @@ -856,6 +856,15 @@ 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. +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. @@ -956,4 +965,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/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 20e1c532c..6106c28f7 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; @@ -96,9 +97,38 @@ async void GenerateSeed() if(!tokenSource.IsCancellationRequested && output.success) { var flags = config.SerializeFlags(); - var basename = $"Z2_{config.Seed}_{flags}"; - var filename = basename + ".nes"; - await files.SaveGeneratedBinaryFile(filename, output.romdata!, Main.OutputFilePath); + var version = Assembly.GetEntryAssembly()!.GetName().Version!; + var versionstr = $"{version.Major}.{version.Minor}.{version.Build}"; + var filename = OutputFilenameFormatter.Format(config.OutputFilenameTemplate, flags, config.Seed, randomizer.Hash, version: versionstr); + var basename = Path.GetFileNameWithoutExtension(filename); + if (string.IsNullOrEmpty(basename)) + { + basename = filename; + } + 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 8e45f03fa..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; @@ -28,22 +29,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 { @@ -111,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 () => @@ -197,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 diff --git a/CrossPlatformUI/ViewModels/RomFileViewModel.cs b/CrossPlatformUI/ViewModels/RomFileViewModel.cs index 43811cf7e..dec251ba2 100644 --- a/CrossPlatformUI/ViewModels/RomFileViewModel.cs +++ b/CrossPlatformUI/ViewModels/RomFileViewModel.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using ReactiveUI; using SD.Tools.BCLExtensions.CollectionsRelated; +using Z2Randomizer.RandomizerCore; using CrossPlatformUI.Services; namespace CrossPlatformUI.ViewModels; @@ -25,6 +26,14 @@ public byte[] RomData } } + private string message { get; set; } = "Select your Zelda 2 ROM to get started!"; + [JsonIgnore] + public string Message + { + get => message; + set { message = value; this.RaisePropertyChanged(); } + } + [JsonIgnore] public IObservable RomDataObservable => this.WhenAnyValue(x => x.RomData); @@ -59,31 +68,36 @@ private async Task OpenFileInternal(CancellationToken token) if (fileprops.Size <= 1024 * 1024 * 1) { await using var readStream = await file.OpenReadAsync(); - var tmp = new byte[(uint)fileprops.Size]; - var read = await readStream.ReadAsync(tmp, token); - // TODO: Better validation - if (read == 1024 * 256 + 0x10) + byte[] fileData = new byte[(uint)fileprops.Size]; + var read = await readStream.ReadAsync(fileData, token); + try { - RomData = tmp; - if (OperatingSystem.IsBrowser()) - { - // Manually save the state - await App.PersistState(); - } - else + ROM.ValidateVanillaRom(ref fileData); + } + catch (UserFacingException e) + { + Message = e.Message; + return; + } + RomData = fileData; + if (OperatingSystem.IsBrowser()) + { + // Manually save the state + await App.PersistState(); + } + else + { + // This part crashes if run in the browser build + if ((Main.OutputFilePath ?? "") == "") { - // This part crashes if run in the browser build - if ((Main.OutputFilePath ?? "") == "") - { - Main.OutputFilePath = new Uri(file.Path, ".").LocalPath; - } + Main.OutputFilePath = new Uri(file.Path, ".").LocalPath; } - HostScreen.Router.NavigateBack.Execute(); } + HostScreen.Router.NavigateBack.Execute(); } else { - throw new Exception("File exceeded 1MB limit."); + Message = "File exceeded 1MB limit. Please provide an unmodified Zelda 2 ROM (US release) with or without header."; } } 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/CrossPlatformUI/Views/Tabs/SpellsView.axaml b/CrossPlatformUI/Views/Tabs/SpellsView.axaml index 2abd5d472..347ea54f6 100644 --- a/CrossPlatformUI/Views/Tabs/SpellsView.axaml +++ b/CrossPlatformUI/Views/Tabs/SpellsView.axaml @@ -74,6 +74,20 @@ > + + + + + + diff --git a/CrossPlatformUI/Views/Tabs/SpritePreviewView.axaml b/CrossPlatformUI/Views/Tabs/SpritePreviewView.axaml index 5a0f117e3..3bfcaefc4 100644 --- a/CrossPlatformUI/Views/Tabs/SpritePreviewView.axaml +++ b/CrossPlatformUI/Views/Tabs/SpritePreviewView.axaml @@ -63,6 +63,18 @@ SelectedItem="{Binding BeamSprite, Converter={x:Static ui:Util.EnumConvert}}"> + + + + + + Sprite Preview 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.Build.props b/Directory.Build.props index 576f389f9..5f2c87277 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,6 +2,11 @@ net10.0 enable + 11.3.12 + 0.10.4 + 3.13.4 + 2.4.1 + 2.88.9 true false true diff --git a/Directory.Packages.props b/Directory.Packages.props index 10abacc93..7c8797232 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,19 +1,21 @@ - true + true true 5.3.0 7.5.1 - 1.0.7 - 12.0.3 - 3.119.4-preview.1.1 + 1.0.9 + 12.0.4 + 3.119.4 - + + + @@ -30,7 +32,7 @@ - + @@ -39,9 +41,9 @@ - - - + + + @@ -55,10 +57,23 @@ - + - - + + + + + + + + + + + + + + + \ 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/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/Flute.s b/RandomizerCore/Asm/Flute.s new file mode 100644 index 000000000..90918eb29 --- /dev/null +++ b/RandomizerCore/Asm/Flute.s @@ -0,0 +1,436 @@ +.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) + +; 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. +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 RealPalaceNumberTable + +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 +.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 + +; 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 destination area index + 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 destination now (if that's where we're headed) +RevealHiddenDestIfNeeded: + ldy LocationNumber + lda OverworldAreaTable, y + bmi @done + lda HiddenRevealAddr + sta OverworldAreaTable, y +@done: + rts + + +.segment "PRG0", "PRG7" + +.reloc +TwisterDrawCheck: + lda OverworldEnemy0Type, 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 FrameCounter + and #$10 + lsr + lsr + adc #TWISTER_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 + 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: + ; clear the twister slot + ldx OverworldCurrentSlot + 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 + clc + adc #$34 + sta LocationNumber + sta AreaEntranceIndex + ; target region = global slot >> 2 + lda TwisterCurrentPalaceSlot + lsr + lsr +.endif + 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 + ; the area byte table isn't loaded for the destination continent yet, so the + ; 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 hidden destination if we are warping there + jsr RevealHiddenDestIfNeeded +@load: + pla + pla + jmp AreaLoadTrigger + +; 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. + ldx #$07 +@active_check: + lda OverworldEnemy0Type, x + cmp #$04 + ; already have a twister so skip processing it + bne @skip_active + jmp @exit +@skip_active: + dex + bpl @active_check + + ; 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 + 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 list forwards +@scan: + sta R7 + + ldy TwisterCurrentPalaceSlot +.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 + 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 FluteWarpFlags + and PowersOfTwo,y + bne @found_dst +@skip_dst: + dex + bpl @next_dst + ; nothing visited yet, so leave + jmp @exit +@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 RealPalaceNumberTable, y + cmp #$ff ; $ff means no palace at this slot + beq @advance + cmp R6 + beq @foundSlot +@advance: + dey + bpl @next_mapping_entry + ; we couldn't find the mapping? that shouldn't be possible... + jmp @exit +@foundSlot: + sty TwisterCurrentPalaceSlot +.endif + +@spawn: + ; Find a free encounter slot + 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 #TWISTER_ENEMY_ID + sta OverworldEnemy0Type, x + lda #$40 + sta OverworldDemonTimer, x + lda OverworldScrollY + clc + ; 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 + lda #$00 + sta OverworldDemonYVelocity, x +@exit: + ; And also check if its time to do spider/3 eye rock encounter + lda OverworldFacingTerrain + 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 +.org $8368 + jmp $836f +FREE_UNTIL $836f + +.org $8387 + jsr TwisterSpawnPRG1 + +.reloc +.proc TwisterSpawnPRG1 + TwisterSpawnBody +.endproc + + +.segment "PRG2" + +; Skip east continent check +.org $8368 + jmp $836f +FREE_UNTIL $836f + +; Same hook at the same address. +.org $8387 + jsr TwisterSpawnPRG2 + +.reloc +.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/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/Init.s b/RandomizerCore/Asm/Init.s index 88747cacb..2074c6088 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) @@ -44,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) @@ -56,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) @@ -76,10 +80,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) @@ -99,4 +112,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 58046db7d..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 @@ -133,6 +150,7 @@ LagFrameVar = $100 .else UpdateSound = $9000 .endif +UpdateTitleScreenSound = $800a .segment "PRG6" @@ -201,16 +219,19 @@ 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 - jsr UpdateSound -@skip: - rts + lda PendingAudioCall + 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" - .reloc RunAudioFrameOrLagFrame: pha @@ -244,6 +265,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 @@ -270,7 +293,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 @@ -295,9 +317,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 @@ -336,6 +357,7 @@ IncStatTimer: asl adc PalaceRegionIndex tay +.import PalaceMappingTable ldx PalaceMappingTable,y @IncrementTimer: inc StatTimeAtLocation+0,x @@ -347,32 +369,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 @@ -470,7 +467,8 @@ bank1f_reset: sta PrgBankEReg jmp bank1f_reset_part2 -.org $fffc +.org $fffa + .word (Bank1f_Nmi) .word (bank1f_reset) .word (bank1f_reset) @@ -521,7 +519,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 @@ -628,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 @@ -644,6 +647,8 @@ ClearStackRAM: and #$23 sta $0267 + 4,x +.endif ; ENABLE_Z2_MARIO + .segment "PRG7" ; Update the pointers to the bank switches @@ -661,6 +666,74 @@ 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 $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 bg rendering only + lda $FE + ora #$08 + 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 @@ -772,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) @@ -796,3 +868,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/StatTracking.s b/RandomizerCore/Asm/StatTracking.s index d174bc009..5e5382a8a 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 @@ -19,6 +13,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 @@ -44,6 +43,7 @@ SetPreviousFrameAction: sta $80 rts +.ifndef ENABLE_Z2_MARIO .org $95FC ; sty $0400 jsr StatTrackSwordSwipe .reloc @@ -109,7 +109,48 @@ StatTrackDownStab: @Exit: 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 +.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 + +.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" @@ -124,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 @@ -134,7 +181,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 @@ -323,6 +370,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 @@ -363,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 @@ -502,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 @@ -765,14 +825,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: @@ -1326,27 +1417,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 diff --git a/RandomizerCore/Asm/ram.inc b/RandomizerCore/Asm/ram.inc index 0b0d108c0..88a09650d 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 @@ -25,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 @@ -42,12 +79,15 @@ OverworldStepCounter = $26 zp_27 = $27 zp_28 = $28 LinkYPos = $29 +EnemyYPositionLo = $2a Enemy0YPositionLo = $2a +OverworldEnemyYPosition = $2a ; base of overworld demon Y world position array, indexed by slot X Enemy1YPositionLo = $2b Enemy2YPositionLo = $2c Enemy3YPositionLo = $2d Enemy4YPositionLo = $2e Enemy5YPositionLo = $2f +ProjectileYPosition = $30 Projectile0YPosition = $30 Projectile1YPosition = $31 Projectile2YPosition = $32 @@ -60,6 +100,7 @@ zp_38 = $38 LinkProjectileYPositionRelated = $39 zp_3a = $3a LinkXPositionHi = $3b +EnemyXPositionHi = $3c Enemy0XPositionHi = $3c Enemy1XPositionHi = $3d Enemy2XPositionHi = $3e @@ -77,8 +118,11 @@ LinkProjectile1XPositionHi = $49 zp_4a = $4a LinkProjectileXPositionHiRelated = $4b zp_4c = $4c +LinkXPosition = $4d LinkXPositionLo = $4d +EnemyXPositionLo = $4e Enemy0XPositionLo = $4e +OverworldEnemyXPosition = $4e ; base of overworld demon X world position array, indexed by slot X Enemy1XPositionLo = $4f Enemy2XPositionLo = $50 Enemy3XPositionLo = $51 @@ -115,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 @@ -128,16 +172,20 @@ Projectile5XVelocity = $7c LinkProjectile0XVelocity = $7d LinkProjectile1XVelocity = $7e zp_7f = $7f +OverworldScrollY = $7f ; camera Y scroll offset; screen Y = world_Y - OverworldScrollY LinkAnimationFrame = $80 +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 -Projectile0Type = $87 ; [SS] Projectile0Type - [OW] OverworldEnemy5Type -Projectile1Type = $88 ; [SS] Projectile1Type - [OW] OverworldEnemy6Type -Projectile2Type = $89 ; [SS] Projectile2Type - [OW] OverworldEnemy7Type +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 ; Projectile0Type / OverworldEnemy5Type +Projectile1Type = $88 ; Projectile1Type / OverworldEnemy6Type +Projectile2Type = $89 ; Projectile2Type / OverworldEnemy7Type Projectile3Type = $8a Projectile4Type = $8b Projectile5Type = $8c @@ -161,6 +209,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 @@ -168,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 @@ -194,6 +244,7 @@ Enemy2Persistence = $be Enemy3Persistence = $bf Enemy4Persistence = $c0 Enemy5Persistence = $c1 +EnemyHP = $c2 Enemy0HP = $c2 Enemy1HP = $c3 Enemy2HP = $c4 @@ -226,6 +277,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 +294,20 @@ 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 + + +BUFFER_OFF = $0301 +PPUADDR_HI = $0302 +PPUADDR_LO = $0303 +BUFFER_LEN = $0304 +BUFFER_DAT = $0305 + LinkXSubpixel = $03d6 Enemy0XSubpixel = $03d7 Enemy1XSubpixel = $03d8 @@ -245,6 +317,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,12 +340,20 @@ LinkIFrameTimer = $500 timer_501 = $0501 ; $501 through $50d are timers that are decreased each frame until 0 LinkEntrySlideTime = $0503 SwordAttackTimer = $050a +InjuryTimer = $050c +OverworldDemonTimer = $050e ; overworld demon active flag/timer, indexed by slot X; non-zero = active EncounterSpawnTimer = $0516 RNG = $051b MapNumber = $0561 +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 ; 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 LinkProjectile0Timer = $05ca LinkProjectile1Timer = $05cb ; Always 0 since Fire projectiles don't expire @@ -272,18 +364,29 @@ 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) 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 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 @@ -310,7 +413,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 @@ -340,6 +443,7 @@ HaveFlute = $0789 HaveCross = $078a HaveHammer = $078b HaveMagicKey = $078c +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 @@ -349,10 +453,302 @@ 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 +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 + +; LocationNumber will later be changed to the side-scrolling location +; 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 +.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 + +; 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 + +; 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 PlayerChrBank +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 +RESV StarInvincibleTimer +FRAME_TIMER_COUNT = StarInvincibleTimer - Timers +ALL_TIMER_COUNT = StarInvincibleTimer - Timers + +RESV SpriteShuffleOffset +;RESV CurrentCHRBank +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 +; have to keep this separate from the tail wag since they can proc at the same time +RESV Poof_CtlHold + +RESV JustThrownHammer +RESV TailSwingTimer +RESV TailWagCount +; Not in the standard timer place so we can tick it down manually +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. +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 + +; 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 +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/z2ft.s b/RandomizerCore/Asm/z2ft.s index 8229de4c7..7a67985e4 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,11 +22,13 @@ UpdateSound = $878c pha lda #6 - 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 @@ -31,6 +36,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 @@ -41,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/CustomTitleScreen.s b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s new file mode 100644 index 000000000..849a58410 --- /dev/null +++ b/RandomizerCore/Asm/z2mario/CustomTitleScreen.s @@ -0,0 +1,820 @@ +.include "z2r.inc" + +.import GameRoutines, PlayerGfxHandler, DrawMetasprite, PlayerBankTable +.import SwapToPRG0, SwapToSavedPRG, SwapPRG +.import ClearExtraRAMOnScreenTransition, ClearCollisionRAM, BankSwitchMarioCHR + +;; 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 + + +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 + +; 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 +RESV TitleIrqYPosition + +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 + +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 BankedInitTitleScreen + +.org $AE41 + jsr BankedInitTitleScreen + +.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 RunTitleMain + jmp $A76A +FREE_UNTIL $A76A + +.org $AB6D + jsr RunTitleMain + jmp $ABA0 +FREE_UNTIL $ABA0 + +.segment "PRG7" +.reloc +RunTitleMain: + lda #$e ; use the same bank the extended + sta CurrentPrgBank + jsr SwapPRG + jsr SetTitleSplitIrq + jsr UpdateTitleSprites + lda #5 + sta CurrentPrgBank + jmp SwapPRG + +.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 +UpdateTitleSprites: + lda #0 + sta CurrentOAMOffset + + jsr RunCurrentTitleState + + 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 +RunCurrentTitleState: + lda TitleState + asl + tax + lda TitleStateTable+1,x + pha + lda TitleStateTable,x + pha + rts + +.reloc +TitleStateTable: + .word WaitForFirstSpawn-1 + .word CheckForSpawnMario-1 + .word PipeRises-1 + .word MarioRises-1 + .word MarioLeavesScreen-1 + +.reloc +WaitForFirstSpawn: + lda TitleStage + cmp #2 + bne + + ; Move to the next state + inc TitleState + + + rts + +.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 +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 "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 + beq + + ; don't reinitalize if we have looped once + rts + + + 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 + + lda #$07 + sta ShootingStarY + lda #$28 + sta ShootingStarX + 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 +@collisionloop: + sta $6000-1,y + sta $6000-1+$d0,y + sta $6000-1+$d0*2,y + sta $6000-1+$d0*3,y + dey + bne @collisionloop + + jmp SetupTitleMario ; jumping here to split the function so it fits better + +.reloc +SetupTitleMario: + ; wipes timers, size/state flags, boss ram, etc + jsr ClearExtraRAMOnScreenTransition + jsr ClearCollisionRAM + + lda #$28 ;store value here + sta VerticalForceDown ;for fractional movement downwards + ; Set our custom floor bytes + ldx #0 + 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 + stx PipeYPositionHi + + ldx #8 + ; PlayerCtrlRoutine index in the GameRoutines jump table + stx GameEngineSubroutine +@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 +BarebonesTitleScreenRNG: + lda seed + asl a + bcs @NoEor + eor #$46 +@NoEor: + adc #$EB + sta seed + rts + +;; --- +; Drawing routines + +.reloc +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: + jsr SwapToPRG0 + .import DecrementMarioTimers, ProcessMarioInput + + 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 + + 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 "PRG1C", "PRG1D" +.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 +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 + 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 + +; ---- +; IRQ handler +.reloc +SetTitleSplitIrq: + lda $27 + and #$0f + sta TitleIrqYPosition + + ; 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 +; 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" + +; 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/boss.s b/RandomizerCore/Asm/z2mario/boss.s new file mode 100644 index 000000000..e9431f12e --- /dev/null +++ b/RandomizerCore/Asm/z2mario/boss.s @@ -0,0 +1,1353 @@ + + +.include "z2r.inc" + +.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 +.import METASPRITE_FIREBALL_FRAME_1, METASPRITE_FIREBALL_FRAME_2 +.import METASPRITE_HAMMER_FRAME_1, METASPRITE_HAMMER_FRAME_2 + +.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 settings: + +; Frames the boss spends standing still after each top-level state change. +STATE_SWITCH_DELAY = 50 + +; 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 = 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 + +; 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 +.org $d57f + jsr DontRunHammerFireball +.reloc +DontRunHammerFireball: + cmp #METASPRITE_FIREBALL_FRAME_1 + bcs @HammerFireball + sec + sbc #1 + rts +@HammerFireball: + pla + pla + rts + +.segment "PRG5", "PRG7" + +; Patch the spot that "kicks" mario shadow out and use it +; 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 +.org $9a7f + jsr BossGraphicsHandler + +.org $9871 + jsr BossLandedInCutscene +.reloc +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 + sta EnemyFacingDir + rts + +.segment "PRG5" +.org $988e + jsr RunBoss + 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 + .byte $00 + +.segment "PRG5", "PRG7" +.reloc +InitShadowBoss: + ; Clear all boss RAM + lda #0 + ldx #BOSS_RAM_SIZE + @loop: + sta boss_animation,x + dex + bpl @loop + ldx #0 ; Dark Link entity is always at enemy slot 0 + ; Set HP + lda #BOSS_HP_INIT + sta EnemyHP,x + lda #JUMPING + sta boss_animation + lda #CHR_BIGMARIO + sta boss_ChrBank + inc ReloadCHRBank + + ; Face left + ldx #0 + lda #2 + sta Enemy_MovingDir,x + sta EnemyFacingDir,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 #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 + + 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 #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 + 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 + + ; Display boss HP bar with a divisor of 10 + lda #80 / 8 + 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 + + @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 + 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 #0 + + 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] + + lda Enemy_State,x + and #%01000000 ; check airborne flag + beq @nograv + jsr MoveD_EnemyVertically ; apply gravity + lda #JUMPING + sta boss_animation + @nograv: + lda boss_invincibility_timer + bne @decrement_timer + 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 + and #%11101111 + sta $a8,x + dec boss_invincibility_timer + rts +AnimateKilledBoss: + inc $63 + rts + +.reloc +HandleBossMovement: + ; If the boss is invincible (just got stomped), force-clear any pending + ; transition delay. + lda boss_invincibility_timer + beq + + lda #0 + sta boss_transition_delay ++ + + 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: + lda #JUMPING + sta boss_animation + jmp MoveEnemyHorizontally +@run_state: + 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 BossFacePlayer ; aim at Mario before shooting + 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 + lda #STATE_SWITCH_DELAY + sta boss_transition_delay + + @keep_state: + dec boss_state_timer + rts + +.reloc + +MovBossRunning: + jsr BossSetWalkingAnim + + ; 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 #1 + beq @facing_right + + lda Enemy_X_Position,x + 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 @move ; still moving left: continue + jmp @flip ; crossed zero / reached zero: flip + +@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 + beq @flip + bmi @flip + jmp @move ; still moving right: continue + +@flip: + jmp MovChangeState + +@move: + jmp MoveEnemyHorizontally + +MovChangeState: + ; Running -> idle. + lda #$b0 + sta boss_state_timer + lda #STANDING + sta boss_animation + lda #0 + sta boss_state + lda Enemy_MovingDir,x + eor #%00000011 + sta Enemy_MovingDir,x + sta EnemyFacingDir,x + rts + +.reloc +MovBossMiddle: + lda boss_substate + beq MovGoToMiddle + + 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 + + @no_need_for_p: + ; Face boss left before jumping off platform + lda #0 + sta boss_bridge + + lda #2 + sta Enemy_MovingDir,x + sta EnemyFacingDir,x + + ; Small hop off the platform + lda #<(-(20)) + sta Enemy_X_Speed,x + + 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 hammer from the boss with a random speed + lda FrameCounter + and #%00010111 + bne @done + jmp BossSpawnHammer + @done: + rts + +MovGoToMiddle: + jsr BossSetWalkingAnim + + ; 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 + bcs @right_of_target + + ; 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 $00 + cmp #PLATFORM_JUMP_DISTANCE + bcs @walk_right_full + lda #JUMP_X_SPEED + sta Enemy_X_Speed,x + jmp @start_jump + +@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 $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 @start_jump + +@on_target_jump_up: + lda #0 + sta Enemy_X_Speed,x + 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 ; 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: + lda FrameCounter + and #%00000011 + cmp #%00000011 + bne @init_anim + inc boss_walking_anim + lda boss_walking_anim + cmp #WALKING+3 + bne @init_anim + lda #WALKING + sta boss_walking_anim + @init_anim: + lda boss_walking_anim + 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 ; face right + bcs @store + lda #2 ; player is left of boss + @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 + sta Square1SoundQueue + lda Enemy_State,x + ora #%01000000 ; set airborne flag + sta Enemy_State,x + rts + +.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 +; +; 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 + 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: + ; 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 + sta PrgBankAReg + pla + sta NmiBankShadow8 + sta PrgBank8Reg + rts + +.segment "PRG5", "PRG7" + +PROJ_TYPE_HAMMER = %00010000 +PROJ_ATTR_FIREBALL = %00000001 +PROJ_ATTR_HAMMER = PROJ_ATTR_FIREBALL | PROJ_TYPE_HAMMER + +.reloc +FindFreeBossProjSlot: + ldy #EnemyProjectileOffset + @loop: + lda EnemyProjectileType - EnemyProjectileOffset,y + beq @found + iny + cpy #EnemyProjectileOffset + ENEMY_PROJECTILE_COUNT + bcc @loop + ldy #$ff + @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 FindFreeBossProjSlot + bmi @done + lda #Sfx_Fireball + sta Square1SoundQueue + lda #4 + sta boss_shootanim ; trigger shoot animation pose + + ; Position: emit from boss center + lda Enemy_Y_Position,x + 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 + lda #1 + sta SprObject_Y_HighPos,y + + ; Direction follows boss facing (already aimed by BossFacePlayer) + lda Enemy_MovingDir,x + 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 + ; 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 + lda #METASPRITE_FIREBALL_FRAME_1 + sta EnemyProjectileType - EnemyProjectileOffset,y + + ldx #0 + @done: + rts + +.reloc +; 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 #Sfx_Fireball + sta Square1SoundQueue + + ; 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 + + ; Random X-speed magnitude in 0..5, sign points toward Mario. + lda RNG + 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 + 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 + + ; initial upward velocity + lda #<(-6) + sta SprObject_Y_Speed,y + + 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 + + ldx #0 + @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: + ldy #EnemyProjectileOffset + @loop: + sty $11 + 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 + + ; 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. + 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 + cmp #FIREBALL_GROUND_Y + bcc @no_bounce + lda #FIREBALL_GROUND_Y + sta SprObject_Y_Position,y + 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 + cmp #248 + bcs @kill + lda SprObject_Y_Position,y + 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 SprObject_Y_Position,y + sta $05 + lda #8 + sta $06 + sta $07 + jsr bank7_LoadLinkHitbox + jsr bank7_CollisionTest + ldy $11 + bcc @next + ldx #0 + lda $a8,x + ora #$10 + sta $a8,x + jsr bank7_LinkCollision + + @kill: + lda #0 + sta EnemyProjectileType - EnemyProjectileOffset,y + @next: + ldy $11 + iny + cpy #EnemyProjectileOffset + ENEMY_PROJECTILE_COUNT + bcs @skip + jmp @loop + @skip: + ldx #0 + rts + +.segment "PRG7" +.reloc +BossDrawProjectiles: + ldy #EnemyProjectileOffset + @loop: + lda EnemyProjectileType - EnemyProjectileOffset,y + beq @next + lda FrameCounter + and #%00000100 + bne @use_frame_2 + lda SprObject_SprAttrib,y + and #PROJ_TYPE_HAMMER + beq @set_fb1 + lda #METASPRITE_HAMMER_FRAME_1 + .byte $2c + @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 + + 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: + + ; Keep the current object we are processing around in $11 for the next loop + ldx EnemyProjectileType - EnemyProjectileOffset,y + sty $11 + jsr DrawMetasprite + ldy $11 + @next: + iny + cpy #EnemyProjectileOffset + ENEMY_PROJECTILE_COUNT + bcc @loop + rts + +.segment "PRG5", "PRG7" +.reloc +KillBoss: + lda boss_counter + 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 #<(-6) + sta Enemy_Y_Speed,x + lda #0 + sta Enemy_X_Speed,x + lda #$7f + sta boss_counter + ; vanilla boss kill behavior + lda #$C0 + sta $074B ; set flash timer + lda #$04 ; set sfx + sta $EC + lda #$14 + sta $0751 + lda #$FF + sta $504 ; stop link from moving + ; 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 + dec boss_counter + bne @done ; still in animation window + dec EnemyHP,x + @done: + ldx #0 + rts + +.reloc +DamagePlayer: + lda $a8,x + ora #$10 ; set flag indicating link was hit by this enemy? + 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 +bank7_LoadLinkHitbox = $E975 ; loads Link/Mario body hitbox into ZP $00-$03 +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 + ; 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 + bmi @try_body_contact + ; Mario is falling so check if we overlap + jsr bank7_LoadLinkHitbox + jsr bank7_CollisionTest + bcc @no_contact + 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 + bcs @contact_damage +@no_contact: + rts + +@contact_damage: + ; Mario touched boss without stomping: hurt Mario via vanilla damage system + lda $a8,x + ora #$10 + sta $a8,x + jmp bank7_LinkCollision + +.reloc +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 + beq + + clc + rts ++ + inc boss_screech + lda EnemyHP,x + sec + sbc $00 + bcs + + lda #0 ; floor at 0 ++ + sta EnemyHP,x + beq + + ; play stab sfx except for last hit. works around a weird bug i didn't bother to fix + lda #$10 + sta Z2NoiseSoundQueue + + + 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 + ; and clears the boss state timer if we are doing something + lda boss_state + bne @done + sta boss_state_timer +@done: + rts + +.segment "PRG7" +.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 + + +; 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/fireball_explosion.chr b/RandomizerCore/Asm/z2mario/fireball_explosion.chr new file mode 100644 index 000000000..6c8fc2bab Binary files /dev/null and b/RandomizerCore/Asm/z2mario/fireball_explosion.chr differ diff --git a/RandomizerCore/Asm/z2mario/integration.s b/RandomizerCore/Asm/z2mario/integration.s new file mode 100644 index 000000000..da3c1d9a8 --- /dev/null +++ b/RandomizerCore/Asm/z2mario/integration.s @@ -0,0 +1,2090 @@ + +.include "z2r.inc" + +.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 + +; Remove unused code after we gutted link's movement +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 = $bf +STATUE_FALL_RESTORE = $70 +STAR_DURATION = 240 ; 4 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 + jsr SwapMarioCHRBanks +.reloc +SwapMarioCHRBanks: + jsr BankSwitchMarioCHR + jmp SpellCastingRoutine + +.reloc +BankSwitchMarioCHR: + lda ReloadCHRBank + beq + +; .repeat 8, I +; lda CurrentCHRBank + I +; sta SpChrBank0Reg + I +; .endrepeat + ; brk + ; 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 + asl + asl + clc + adc #4 + sta BgChrBank0Reg + lda #0 + sta ReloadCHRBank + + + 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 +.org $C40D + 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 SprObject_Y_HighPos + sta PlayerFacingDir + 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 boss_animation ; and clear the dark link flag + sta $075c ; original code + rts + +; 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 + ; Clear all spells while here too + lda #0 + sta $076f + sta $070f + lda #$0b ; player death subroutine + sta GameEngineSubroutine + rts + +; patch death screen to bank the metasprites properly +.org $c9f1 + jsr PatchLinkDeathSprite + nop + +.reloc +PatchLinkDeathSprite: + lda #METASPRITE_SMALL_MARIO_DEATH + 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 + 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 $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 + +; "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 @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 + +.org $8E8A + rts ; Disable the vanilla spell spell behavior that turns everything to bots + +.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 + +.segment "PRG7" + +; patch the link draw routine to skip drawing him with the vanilla code +.org $EC02 ; ldx $11 lda $29,x + jsr PatchLinkDrawRoutine +; 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) + +.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 +BankPatchLinkDrawRoutine: + lda $0769 + bne + + jsr PatchLinkDrawRoutine + jmp @Exit + + + jsr SwapToPRG0 + jsr $EC02 ; let the double return go back to here + jsr SwapToSavedPRG + rts +@Exit: + ; double return to skip the OG drawing code + pla + 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 + jmp *+3 + + +.segment "PRG7" + +.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: +; 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 + + ldx ObjectMetasprite + beq @skipplayer + ; Apply swing override if we are a tanooki. + lda PlayerFacingDir + 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 + 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 + + + ; Update the player bank if the metasprite has changed +.import PlayerBankTable + ldy ObjectMetasprite + lda PlayerBankTable,y + cmp PlayerChrBank + beq + + sta PlayerChrBank + inc ReloadCHRBank + + + ; If the tanooki suit is on, then switch to the big version + lda $d0 + beq + + lda PlayerChrBank + cmp #CHR_BIGMARIO + bne + + clc + adc #CHR_TANOOKI - CHR_BIGMARIO + sta PlayerChrBank + inc ReloadCHRBank + + +@skipplayer: + ; While we are here, lets draw the fireball/hammer sprites too + ldy #ProjectileOffset + sty R2 +@fireballLoop: + ldx ObjectMetasprite,y + beq @nofireball + jsr DrawMetasprite +@nofireball: + inc R2 + 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 +.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 MarioScrollFreeze ; 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 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 MarioScrollFreeze +@alreadyLocked: + lda #STATUE_CHANGE_DURATION + sta StatueChangeTimer + lda Square2SoundQueue + ora #Sfx_StatuePoof + sta Square2SoundQueue + rts + +.reloc + +.proc ProcTanookiSwing +.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 + + 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 A_B_Buttons + and #B_Button + beq @done + and PreviousA_B_Buttons + bne @done + + 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 TailSwingTimer + lda Square1SoundQueue ; queue the tail-wag SFX on swing start + ora #Sfx_TailWag + sta Square1SoundQueue +@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 TailSwingTimer + beq @done ; not swinging + cmp #16 + bcs @done + lda #METASPRITE_BIG_MARIO_STANDING + sta ObjectMetasprite + + lda TailSwingTimer + 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 +.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 + + ; 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 +@noSwing: + + 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 +@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 + 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 + + ; 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 + ora Atr + sta Atr + + jmp MetaspriteRenderLoop + +.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 +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 +OverworldLateInit: + sta $0736 ; Sets game mode + lda #$18 ; player palette + sta $69de + 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 + txa + pha + 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 @writeCommand + ldx #7-1 +@writeCommand: + ; write a palette update for next NMI + ldy BUFFER_OFF + dey + - + iny + inx + lda PlayerRegularPalettePPUCommand,x + sta PPUADDR_HI,y + bpl - + 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 BUFFER_OFF,y ++ + 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 +.org $8cf4 + jsr DontClearMagicState +.reloc +; 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 #$12 + sta $76f + tya + jmp UpdatePlayerPalette + +; 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 +.reloc +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 #$28 ;store value here + sta VerticalForceDown ;for fractional movement downwards if necessary + jmp UpdatePlayerPalette + + +; Patch link's standard routine +.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 +; 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 + ; 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 + ; 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. + lda #1 + sta $0B + jsr $E726 + 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 +.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 + ; also clear the jump spell too if we shrink + lda $76f + and #~$12 + sta $76f + jsr UpdatePlayerPalette + + ; Set player routine to ChangeSize + lda #$0a + sta GameEngineSubroutine +; lda #8 +; sta InjuryTimer + ; we will unlock the scroll freeze after the size change animation finishes + inc MarioScrollFreeze + lda #1 + sta Player_State + ; sta PlayerChangeSizeFlag + lda #0 + sta ScrollAmount + ; 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 + sta Square1SoundQueue ;load bump sound + + + rts + +.segment "PRG0", "PRG7" + +; Make mario grow when his HP increases enough + +.org $cb23 ; patches over the "set life after level" + jsr LifeOrMagicSetToMax +.reloc +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 + 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 freeze after the size change animation finishes +; lda #1 + inc MarioScrollFreeze + 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 +PatchLinkMain: + ldx #$00 + stx JustThrownHammer ; Clear the just thrown hammer flag + 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 / entrance move timer +; beq @notmoving +; ldy $701 +; lda $93b5,y +; rts +; @notmoving: + jsr DecrementMarioTimers + + ; 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 ProcHammerTime + jsr ProcFireball_Bubble ;process fireballs and air bubbles + jsr ProcStarPower + jsr ProcTanookiStatue + + jsr ProcessMarioInput + + ; 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 + ; Check for right lock + cmp #$e5 + bcc @SkipScrollLock ; You are still in bounds + lda #$e4 + .byte $2c + @LockLeft: + lda #4 + @StopMovement: + sta Player_X_Position + 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 +.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: + ; jsr $ed02 ; attempt to downstab while falling + ; 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 + ; 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 + ; don't stomp on things while star power is running + ; lda StarInvincibleTimer + ; bne @notstabbing + lda Player_Y_Speed + bmi @notstabbing + beq @notstabbing + LDX #0 + JSR $ECEA + CLC + ADC #$25 - 4 ; - 2 cause I don't know + STA HitboxYCoord ; ,x + LDA $CC ; ,x + 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 TailSwingTimer + 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 #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: +@done: + rts + +; Prevent recoil and playing the sword sound when hitting a wall +.org $E268 + BCS $E28B +.org $E273 + jmp $E28B + +.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 +bank7_LoadObjectHitbox = $E942 +bank7_CollisionTest = $E9F9 +.org $e1e0 + ; Check the hammer hitboxes as if they were stabbing blocks + jsr CheckHammerHitboxes +.reloc +CheckHammerHitboxes: + ; Check if the stab is hitting a brick before hammers + lda #0 + sta $0b + ; 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 + + ; Inlined LoadProjectileCollisionBox ($E4BC) but with an offscreen guard + lda Fireball_X_Position,y + sec + sbc ScreenLeft_X_Pos + sta HitboxXCoord + 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 + +; 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 + jsr PatchFireballHitcheck +.reloc +PatchFireballHitcheck: + ldy $11 ; fireball or hammer slot (0 or 1) +PatchFireballHitcheckInternal: + lda Fireball_State,y + and #%01000000 + bne @IsHammer + lda #1 + sta $0b ; 1 = fireball + bne @CollisionCheck ; always taken +@IsHammer: + lda #0 + sta $0b ; 0 = hammer (pass-through) +@CollisionCheck: + inc ProjectileProcessing + jsr $E694 + lda #0 + sta ProjectileProcessing + bcc @NoCollision + ldy $11 + lda Fireball_State,y + and #%01000000 + 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: + 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 + +; Skip over something? this was related to projectiles... +.org $d50d + jmp *+3 + +; 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 +; 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 + jmp ClearSpotInCollisionRam +.reloc +.export ClearSpotInCollisionRamJustOneSpot +ClearSpotInCollisionRam: + ; A is always #$42 here? + sta ($00),y +ClearSpotInCollisionRamJustOneSpot: + 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 + +; 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 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 + +.org $C629 + jsr UpdateCollisionFromMap +.reloc +UpdateCollisionFromMap: + sta $0735 + ldx #$d0 +@loop: + lda $6000-1,x + jsr CheckForSolidMTiles + bcc @skip1 + sta COLLISION_TILES-1,x +@skip1: + lda $6000-1+$d0,x + jsr CheckForSolidMTiles + bcc @skip2 + sta COLLISION_TILES-1+$d0,x +@skip2: + lda $6000-1+$d0*2,x + jsr CheckForSolidMTiles + bcc @skip3 + sta COLLISION_TILES-1+$d0*2,x +@skip3: + lda $6000-1+$d0*3,x + jsr CheckForSolidMTiles + bcc @skip4 + sta COLLISION_TILES-1+$d0*3,x +@skip4: + dex + bne @loop + rts + +.reloc +SlightlyModifiedCollisionRoutine: + jsr SwapToSavedPRG + 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 + + +; 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 + +.segment "PRG0", "PRG7" +.org $92A5 + jmp PatchFinishPlayerPaletteCycle +.reloc +PatchFinishPlayerPaletteCycle: + sta $0301 + ldy $074B ; flash frame counter ; If its the last frame of the spell cycle + bne + + ; 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 + + +; Update palette locations + 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 + +; 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 $18 @ $2a16 + +; regular palette color location +UPDATE_BYTE $18 @ $10ea +; shield spell color location +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 + jsr CheckMarioNoiseSfx + +.org $9022 + jsr ClearQueues + 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: + ; $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 + + + ; 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 @ActualZ2Sfx + lda #$ff + 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 +CheckMarioNoiseSfx: + ; 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: + jmp $95A7 ; Original "complex" sfx from z2 + +.segment "PRG7" +; Make mario state "on ground" when riding an elevator +.org $d8cd + jsr ElevatorMakeMarioStateStanding +.reloc +ElevatorMakeMarioStateStanding: + sta $0479 + sta Player_State + rts + +SetObjectRecoil = $e371 + +; Disable vanilla recoil when jumping off enemies +.org $e747 + 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 +.org $E65E + jsr CheckForDownstab + bcc $E66A + jmp SetStompBounce +.assert * <= $E66A + +; Disable recoil when hitting enemy with hammers +.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: + ; 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 + bne @kill + ldy HammerHitFlag,x + beq @keep +@kill: + lda #0 +@keep: + sta $043e,x + rts + +; Make mario bounce off armored enemies +.org $E714 + lda ProjectileProcessing + bne @skip + jsr CheckForDownstab + bcc @skip + lda #$fc ; set player recoil to -4 + jsr SetDownstabStatTracking +@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 + sta $057D + jsr ApplyStatueBounceForce + ; stomped an enemy so inc stats as well + inc StatDownStabCount+0 + bne @Exit + inc StatDownStabCount+1 +@Exit: + rts + +; This check for upstab/downstab and we can just check for downstab in mario instead +.org $e9ba + jsr InyIfDownstabbing + jmp $e9c5 +FREE_UNTIL $e9c5 + +.reloc +InyIfDownstabbing: + jsr CheckForDownstab + bcc + + iny + + + rts + +.reloc +; Stomp physics between SMB1 and SMB3 are quite different, so add extra bounce force +; to make it feel better. the statue is "heavier" so you bounce less. +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 + ; 1 if downstabbing 0 otherwise + lda PlayerIsDownstabbing + 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 DownstabPierceShieldCollision + +.reloc +DownstabPierceShieldCollision: + ; If you are stomping + lda PlayerIsDownstabbing + lsr + bcc @normal + ; And you have downstab + lda HaveStabs + and #HAVE_DOWNSTAB + beq @normal + ; check if we want to stab this kinda enemy + ldx $10 + 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 +@normal: + jmp $E9F9 ; continue the shield collision check + +.segment "PRG3" +.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 + +.segment "PRG7" +.org $d29c + jsr ClearExtraRAMOnScreenTransition +.reloc +ClearExtraRAMOnScreenTransition: + lda #0 + ; 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 + 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 + +; 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 + +.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 +; .segment "PRG4" +; .org $978F +; jmp *+3 + +; .org $AE08 +; jmp *+3 + +; .segment "PRG5" +; .org $9C8E +; jmp *+3 diff --git a/RandomizerCore/Asm/z2mario/map.s b/RandomizerCore/Asm/z2mario/map.s new file mode 100644 index 000000000..a312066e4 --- /dev/null +++ b/RandomizerCore/Asm/z2mario/map.s @@ -0,0 +1,171 @@ + +.include "z2r.inc" + +.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 +FREE_UNTIL $87A2 +.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 000000000..ebd3f2454 Binary files /dev/null and b/RandomizerCore/Asm/z2mario/map_mario.chr differ diff --git a/RandomizerCore/Asm/z2mario/mario.s b/RandomizerCore/Asm/z2mario/mario.s new file mode 100644 index 000000000..bcee7b01d --- /dev/null +++ b/RandomizerCore/Asm/z2mario/mario.s @@ -0,0 +1,2802 @@ + +.include "z2r.inc" + +.import SlightlyModifiedCollisionRoutine +.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_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_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, 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 + +;------------------------------------------------------------------------------------- + +.proc GameRoutines + +; 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) + + .word $0000 ; Entrance_GameTimerSetup + .word $0000 ; Vine_AutoClimb + .word $0000 ; SideExitPipeEntry + .word $0000 ; VerticalPipeEntry + .word $0000 ; FlagpoleSlide + .word $0000 ; PlayerEndLevel + .word $0000 ; PlayerLoseLife + .word $0000 ; PlayerEntrance + .word PlayerCtrlRoutine + .word PlayerChangeSize + .word PlayerInjuryBlink + .word PlayerDeath + .word PlayerFireFlower +.endproc + +.reloc + +; ;------------------------------------------------------------------------------------- +; ; ; .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 + + ; 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 + 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: + lda LinkCollisionBits ; flip collision bits to match mario style + eor #$ff + 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 + 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 + + 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 + + lda LinkCollisionBits ; flip collision bits to match z2 style + eor #$ff + sta LinkCollisionBits + + ; 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 + jmp 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 +; .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 +;------------------------------------------------------------------------------------- +;$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 + bne + + jmp PlayerKilled ++ 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 ++ + 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: + rts ;then leave + +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 + +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 + ; 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 if you are small and fire mario + lda PlayerSize + bne SmallFireMario + lda #METASPRITE_FIRE_MARIO_SWIMMING_STILL_1 + bne SetFrame + SmallFireMario: + 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: + +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 $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 + .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 $00 ; 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 + ; Z2Mario - clear the mario scroll freeze after you change size +; lda #0 + 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 the mario scroll freeze after you change size +; lda #0 + dec MarioScrollFreeze + ; and also reset the injurytimer to extend your invincibility + lda #$20 + sta InjuryTimer + jmp DonePlayerTask +;------------------------------------------------------------------------------------- +; .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 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 + +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 Player_State + bne + + lda #Sfx_PowerUpGrab + sta Square2SoundQueue ;load grow up sound + + + 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 +; 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: + 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 $0000 ; ClimbingSub ; removed to save space + +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 HitboxYCoord + 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: +; 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 + 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: + + ; 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 + 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 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 + 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 + +.segment "PRG0" +;------------------------------------------------------------------------------------- +;$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) + +.export ImposeGravitySprObj +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 + +.export BlockBufferCollision +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: + ; If we are leaving the map, then treat the offscreen blocks as no collision + pla + lda #0 + sta R3 + sta R4 + 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 + ; 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 + bcc DoFootCheck ;if low nybble < 4, branch + ; we can't check solid type here since its banked out + 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 + ; 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 + ; 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 + +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 HitboxYCoord + lda $cc + clc + adc #$10 + sta HitboxXCoord +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 +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 + ; 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 + +.export ImpedePlayerMove +.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 +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: + ; 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 + jsr bank7_stab_brick_at_0E_with_A_and_does_draw +.import PowShockwaveCheck + jmp PowShockwaveCheck +ExitFireballNearby: + rts ;leave! +ExitBackToBump: + pla + pla + jmp SolidOrClimb +ProcFireball_Bubble: +; lda PlayerStatus ;check player's status +; cmp #$02 +; 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 + and PreviousA_B_Buttons + bne ProcFireballs ;if button pressed in previous frame, 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 + 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: +; 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 + 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 + 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 + ; 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 + beq RunFB + lda Player_X_Position ;get player'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 + 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 + tya ; Z2 needs a value set for the knockback value in $6d + 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 + lda #4 ;set vertical speed of fireball + sta Fireball_Y_Speed,x + + lda #0 ; Don't add extra gravity (probs not needed?) + 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 +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: + ; And check if its a player hammer (bit 6) + asl + bcs NoFBall + 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 #1 + sta Fireball_SprAttrib,x + lda ExplosionTiles,y ;get tile number using offset + sta FireballMetasprite,x + 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 + +.export ProcHammerTime +.proc ProcHammerTime + lda $078b ; Check for hammer in inventory + bne + + 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 + ; + + ; 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 @UpdateHammersLocal ;branch if not pressed + and #B_Button ;check if b button just pressed + and PreviousA_B_Buttons + bne @UpdateHammersLocal ;if button pressed in previous frame, branch + ldy Player_Y_HighPos ;if player too high or too low, branch + dey + 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 + inx + lda Fireball_State,x ;load fireball state + beq @SpawnHammer + ;if not inactive, branch +@UpdateHammersLocal: + jmp UpdateHammers +@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 + 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 + 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 + ; 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 + sta Fireball_Y_Position,x + 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 + 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 +HammerXPosition: + .byte $10, $00 +.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 + FireballOffset,x + sta SprObject_X_MoveForce + FireballOffset,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_MovingDir,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_MovingDir,x +; sta Fireball_FacingDir,x +WriteMetasprite: + tya + sta FireballMetasprite,x + rts +.endproc + +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/mario_title.chr b/RandomizerCore/Asm/z2mario/mario_title.chr new file mode 100644 index 000000000..220c206ee Binary files /dev/null and b/RandomizerCore/Asm/z2mario/mario_title.chr differ diff --git a/RandomizerCore/Asm/z2mario/metasprite.s b/RandomizerCore/Asm/z2mario/metasprite.s new file mode 100644 index 000000000..8c0fd9c7c --- /dev/null +++ b/RandomizerCore/Asm/z2mario/metasprite.s @@ -0,0 +1,843 @@ + + +;;;;;;;;;;;;;;;;; +; +; 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 + +.include "z2r.inc" + +.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 + +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 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 +; + + + cpx #LAST_MARIO_METASPRITE + bcc + + ; 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 + 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 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 + + ; 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 + 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 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 + 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 + ; 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/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 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 + lda Sprite_Attributes - 4, x + ora #$20 + sta Sprite_Attributes - 4, x + rts + @bigmario: + lda Sprite_Attributes - 16, x + ora #$20 + sta Sprite_Attributes - 16, x + lda Sprite_Attributes - 12, x + ora #$20 + sta Sprite_Attributes - 12, x +@exit: + 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 + + +.export DrawMetasprite + +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 + +.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 +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 + +.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 +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 + +LAST_MARIO_METASPRITE = METASPRITES_COUNT + +FIREBALL_VRAM_OFFSET = SPRITE_BANK_0 +FIREBALL_PALETTE = $01 +FIREBALL_X_OFFSET = 0 +FIREBALL_Y_OFFSET = -12 +EXPLOSION_VRAM_OFFSET = SPRITE_BANK_1 +EXPLOSION_PALETTE = $00 +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 +; 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 + +; 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 +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 + + +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 + +MetaspriteData "METASPRITE_NULL", $0000 + + +;;;; +; 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)) +.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/music/Battle.dnm b/RandomizerCore/Asm/z2mario/music/Battle.dnm new file mode 100644 index 000000000..e4f044b28 Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/Battle.dnm differ diff --git a/RandomizerCore/Asm/z2mario/music/BattleSwing.dnm b/RandomizerCore/Asm/z2mario/music/BattleSwing.dnm new file mode 100644 index 000000000..de077f4bb Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/BattleSwing.dnm differ diff --git a/RandomizerCore/Asm/z2mario/music/Boss.dnm b/RandomizerCore/Asm/z2mario/music/Boss.dnm new file mode 100644 index 000000000..8c293488d Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/Boss.dnm differ diff --git a/RandomizerCore/Asm/z2mario/music/Cave.ftm b/RandomizerCore/Asm/z2mario/music/Cave.ftm new file mode 100644 index 000000000..328cac02e Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/Cave.ftm differ diff --git a/RandomizerCore/Asm/z2mario/music/Credits.ftm b/RandomizerCore/Asm/z2mario/music/Credits.ftm new file mode 100644 index 000000000..d2c509000 Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/Credits.ftm differ diff --git a/RandomizerCore/Asm/z2mario/music/FinalBoss.dnm b/RandomizerCore/Asm/z2mario/music/FinalBoss.dnm new file mode 100644 index 000000000..5cdcfef2e Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/FinalBoss.dnm differ diff --git a/RandomizerCore/Asm/z2mario/music/GreatPalace.dnm b/RandomizerCore/Asm/z2mario/music/GreatPalace.dnm new file mode 100644 index 000000000..0eafb1ea9 Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/GreatPalace.dnm differ diff --git a/RandomizerCore/Asm/z2mario/music/Indoors.dnm b/RandomizerCore/Asm/z2mario/music/Indoors.dnm new file mode 100644 index 000000000..3bd519e92 Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/Indoors.dnm differ diff --git a/RandomizerCore/Asm/z2mario/music/Overworld.dnm b/RandomizerCore/Asm/z2mario/music/Overworld.dnm new file mode 100644 index 000000000..be646bcbd Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/Overworld.dnm differ diff --git a/RandomizerCore/Asm/z2mario/music/Palace.dnm b/RandomizerCore/Asm/z2mario/music/Palace.dnm new file mode 100644 index 000000000..78d6be3d3 Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/Palace.dnm differ diff --git a/RandomizerCore/Asm/z2mario/music/TitleScreen.ftm b/RandomizerCore/Asm/z2mario/music/TitleScreen.ftm new file mode 100644 index 000000000..090a2c19f Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/TitleScreen.ftm differ diff --git a/RandomizerCore/Asm/z2mario/music/Town.dnm b/RandomizerCore/Asm/z2mario/music/Town.dnm new file mode 100644 index 000000000..85994da7c Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/Town.dnm differ diff --git a/RandomizerCore/Asm/z2mario/music/ZeldaSaved.ftm b/RandomizerCore/Asm/z2mario/music/ZeldaSaved.ftm new file mode 100644 index 000000000..451ecca43 Binary files /dev/null and b/RandomizerCore/Asm/z2mario/music/ZeldaSaved.ftm differ diff --git a/RandomizerCore/Asm/z2mario/sfx.s b/RandomizerCore/Asm/z2mario/sfx.s new file mode 100644 index 000000000..961e014b0 --- /dev/null +++ b/RandomizerCore/Asm/z2mario/sfx.s @@ -0,0 +1,852 @@ + +.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 #$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 + +ResetMarioSfxState: + jsr ResetSquare1Sfx + jsr ResetSquare2Sfx + lda #$00 + sta NoiseSoundBuffer + sta Noise_SfxLenCounter + sta Square1SoundQueue + sta Square2SoundQueue + sta NoiseSoundQueue + rts + +;------------------------------------------------------------------------------------- +.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 + +; ----------------------------------------------------------------------------- +; 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 +; ----------------------------------------------------------------------------- +PlaySMB3Flute: + lda #SMB3FluteSong - SndLev1_DataLongWag + sta Squ1_SfxLenCounter + lda #$90 + sta TailWag_CtlHold + jmp ContinueTailWag +PlayTailWag: + lda #$00 + sta Squ1_SfxLenCounter + 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 SND_SQUARE1_REG+2 + ; jroweboy: full clear the sfx + jmp StopSquare1Sfx + +; 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 + +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 + jmp EmptySfx2Buffer ; jroweboy full clear the sfx + +; 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 + +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 + bcc + + jmp PlaySMB3Flute + + ; was PlaySmackEnemy ;smack enemy + lsr Square1SoundQueue + bcc + + jmp PlayPipeDownInj ;pipedown/injury + + + lsr Square1SoundQueue + bcs PlayFireballThrow ;fireball throw + lsr Square1SoundQueue + bcc + + jmp PlayTailWag ;tanooki tail-wag (was flagpole-slide) + + + +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 + bcc + + jmp ContinueTailWag ;tanooki tail-wag (was flagpole-slide) + + +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 + stx Squ1_SfxLenCounter + ldx #$10 + stx SND_SQUARE1_REG + ldx #SWEEP_OFF + stx SND_SQUARE1_REG+1 +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 + stx Squ2_SfxLenCounter + +StopSquare2Sfx: + ldx #$10 + stx SND_SQUARE2_REG + ldx #SWEEP_OFF + stx SND_SQUARE2_REG+1 +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 + beq + + jmp ContinueExtraLife + + + ldy Square2SoundQueue ;check for sfx in queue + beq CheckSfx2Buffer + sty Square2SoundBuffer ;if found, put in buffer and check for the following + bpl + + jmp PlayStatuePoof ;tanooki statue change (was bowser fall) + + + lsr Square2SoundQueue + bcc + + jmp PlayCoinGrab ;coin grab + + + lsr Square2SoundQueue + bcc + + jmp PlayGrowPowerUp ;power-up reveal + + + lsr Square2SoundQueue + bcc + + jmp PlayGrowVine ;vine grow + + + lsr Square2SoundQueue + bcc + + jmp PlayBlast ;fireworks/gunfire + + + lsr Square2SoundQueue + bcc + + jmp PlayTimerTick ;timer tick + + + lsr Square2SoundQueue + bcc + + jmp PlayPowerUpGrab ;power-up grab + + + lsr Square2SoundQueue + 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 + bcc + + jmp 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 + bcc + + jmp ContinuePowerUpGrab ;power-up grab + + + lsr + bcc + + jmp 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 +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 + +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 + +; 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/z2mario/sprites_mario_hammer.chr b/RandomizerCore/Asm/z2mario/sprites_mario_hammer.chr new file mode 100644 index 000000000..d5448d735 Binary files /dev/null and b/RandomizerCore/Asm/z2mario/sprites_mario_hammer.chr differ diff --git a/RandomizerCore/Asm/z2mario/sprites_mario_tanooki.chr b/RandomizerCore/Asm/z2mario/sprites_mario_tanooki.chr new file mode 100644 index 000000000..6fe339e08 Binary files /dev/null and b/RandomizerCore/Asm/z2mario/sprites_mario_tanooki.chr differ diff --git a/RandomizerCore/Asm/z2mario/tanooki_tail.chr b/RandomizerCore/Asm/z2mario/tanooki_tail.chr new file mode 100644 index 000000000..24ccdfe0d Binary files /dev/null and b/RandomizerCore/Asm/z2mario/tanooki_tail.chr differ diff --git a/RandomizerCore/Asm/z2r.inc b/RandomizerCore/Asm/z2r.inc index 35a850342..8a51247cb 100644 --- a/RandomizerCore/Asm/z2r.inc +++ b/RandomizerCore/Asm/z2r.inc @@ -1,37 +1,7 @@ -.macpack common +.ifndef _Z2R_INC +_Z2R_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 +.macpack common ;;; Standard register names PPUCTRL = $2000 @@ -50,6 +20,22 @@ JOY2 = $4017 ;;; Include RAM values before any constants .include "ram.inc" + +R0 = $00 +R1 = $01 +R2 = $02 +R3 = $03 +R4 = $04 +R5 = $05 +R6 = $06 +R7 = $07 + +SND_REGISTER = $4000 +SND_SQUARE1_REG = $4000 +SND_SQUARE2_REG = $4004 +SND_TRIANGLE_REG = $4008 +SND_NOISE_REG = $400c + ;;; MMC5 register names and associated constants PrgBankModeReg = $5100 PRG_BANK_MODE_8KB_BANKS = 3 @@ -104,78 +90,85 @@ LineIrqStatusReg = $5204 DISABLE_SCANLINE_IRQ = 0 ENABLE_SCANLINE_IRQ = $80 +HAVE_UPSTAB = $04 +HAVE_DOWNSTAB = $10 + MulLoReg = $5205 MulHiReg = $5206 NewSideviewInit = $8cec ClearNametables = $d266 JumpEngine = $d385 - -;; 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 -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 - -.assert RES_BASE + RES_OFFSET < $01ad +EnemyFacingDirection = $DC91 +LoadPlayerShieldCollisionBox = $e9d8 +ObjectCollisionCheck = $e9f9 +MoveEnemyHorizontally = $deb8 +MoveObjectHorizontally = $D1CE +CheckForSolidMTiles = $850C +CheckSideviewTransition = $E14A + +.ifdef ENABLE_Z2_MARIO +;; Constants + +CHR_BIGMARIO = $6a +CHR_MARIOACTION = $6b +CHR_SMALLMARIO = $6c +CHR_SMALLFIRE = $6d +CHR_TANOOKI = $6e +CHR_TANOOKI_ACTION = $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 +;sq1 sfx +Sfx_SmallJump = %10000000 +Sfx_TailWag = %01000000 +Sfx_Fireball = %00100000 +Sfx_PipeDown_Injury = %00010000 +Sfx_EnemySmack = %00001000 +Sfx_EnemyStomp = %00000100 +Sfx_Bump = %00000010 +Sfx_BigJump = %00000001 + +;sq2 sfx +Sfx_StatuePoof = %10000000 +Sfx_ExtraLife = %01000000 +Sfx_PowerUpGrab = %00100000 +Sfx_TimerTick = %00010000 +Sfx_Blast = %00001000 +Sfx_GrowVine = %00000100 +Sfx_GrowPowerUp = %00000010 +Sfx_CoinGrab = %00000001 +; noise sfx +Sfx_BrickShatter = %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 @@ -187,42 +180,22 @@ 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 -StatTrackingBase = $6383 +COLLISION_TILES = $6350 + +;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 -; 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 @@ -248,6 +221,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 @@ -259,13 +233,133 @@ 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 +Fireball_SprAttrib = SprObject_SprAttrib + FireballOffset +;Fireball_FacingDir = PlayerFacingDir + 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 + + +;;; 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 ; 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 + + + -.assert RES_OFFSET + StatTrackingBase < $6957 +.endif diff --git a/RandomizerCore/CustomTexts.cs b/RandomizerCore/CustomTexts.cs index 0e09c2b12..79848ddd7 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, @@ -528,7 +605,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) { @@ -600,10 +677,11 @@ public static List GenerateTexts( } } - if (props.HelpfulHints) + + 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.HelpfulHints, props.UseCommunityText, props.MarioMode); } if (props.UseCommunityText) @@ -699,7 +777,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 +797,24 @@ 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) + { + // 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("PIERCE$SHIELDS$WITH YOUR$STOMP."); + case Collectable.UPSTAB: + 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: + return new Text("SAY THE$MAGIC WORD$AND FEEL$THE POWER"); + default: + break; + } } //Non community-text spells use the vanilla text corresponding to the spell you get. return new Text(texts[wizardTextIndexesBySpell[collectable]].RawText); @@ -768,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 helpfulHints, 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 = [ @@ -794,6 +901,8 @@ private static void GenerateKnowNothings(List hints, List placedIndex List moving = [ + .. genericEastMoving, + .. genericWestMoving, .. rauruMoving, .. rutoMoving, .. sariaMoving, @@ -804,50 +913,56 @@ 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 - { - hint = defaultKnowNothing; - } - hints[textIndex] = hint; - if (textIndex == talkingAcheIndexTalking) - { - hints[talkingAcheIndexSleeping] = hint; - } - else if (textIndex == talkingBotIndexTalking) - { - hints[talkingBotIndexSleeping] = hint; - } } } 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; } } @@ -1023,7 +1138,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 +1153,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; @@ -1078,6 +1193,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/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/FlagPasteParser.cs b/RandomizerCore/FlagPasteParser.cs new file mode 100644 index 000000000..91628fb1d --- /dev/null +++ b/RandomizerCore/FlagPasteParser.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Z2Randomizer.RandomizerCore.Flags; + +namespace Z2Randomizer.RandomizerCore; + +public static class FlagPasteParser +{ + private static readonly HashSet ValidFlagCharacters = [.. FlagBuilder.ENCODING_TABLE, '$']; + + public static string? ExtractFlags(string input) => Parse(input).Flags; + + public static string? ExtractSeed(string input) => Parse(input).Seed; + + public static bool IsValidFlagString(string flags) => TryNormalizeFlagString(flags, out _); + + // Replace anything that isn't a valid flag string character with a space, + // tokenize on the resulting whitespace, then pick out the flags and seed. + public static (string? Flags, string? Seed) Parse(string input) + { + if (string.IsNullOrWhiteSpace(input)) + { + return (null, null); + } + + var cleaned = string.Concat( + input.Select(character => ValidFlagCharacters.Contains(character) ? character : ' ')); + var tokens = cleaned.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + string? flags = null; + var flagIndex = -1; + for (var i = 0; i < tokens.Length; i++) + { + if (TryNormalizeFlagString(tokens[i], out var normalizedFlags)) + { + flags = normalizedFlags; + flagIndex = i; + break; + } + } + + // A seed is only meaningful alongside flags, so don't look for one until + // the flags have been found. + if (flags is null) + { + return (null, null); + } + + for (var i = 0; i < tokens.Length; i++) + { + if (i != flagIndex && tokens[i].Length >= 6 && tokens[i].All(char.IsDigit)) + { + return (flags, tokens[i]); + } + } + + return (flags, null); + } + + private static bool TryNormalizeFlagString(string flags, out string normalizedFlags) + { + normalizedFlags = string.Empty; + if (string.IsNullOrWhiteSpace(flags)) + { + return false; + } + + var trimmedFlags = flags.Trim(); + if (trimmedFlags.Any(character => !ValidFlagCharacters.Contains(character))) + { + return false; + } + + try + { + normalizedFlags = new RandomizerConfiguration(trimmedFlags).SerializeFlags(); + return normalizedFlags == trimmedFlags; + } + catch (Exception) + { + return false; + } + } +} diff --git a/RandomizerCore/Hyrule.cs b/RandomizerCore/Hyrule.cs index f265edda7..d773d94ae 100644 --- a/RandomizerCore/Hyrule.cs +++ b/RandomizerCore/Hyrule.cs @@ -189,6 +189,7 @@ public RandomizerProperties Props private static readonly Js65Options assemblerOptions = new() { // Must exist for the FileResolve callback to be called + lineContinuations = true, includePaths = [""], // debugLevel = 0, // debugLevel = 1, @@ -204,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 @@ -222,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); @@ -265,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); @@ -459,7 +466,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); @@ -537,6 +544,10 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC SanitizeHashCharacters(z2Hash); } + if (Z2MarioModeEnabled && !IS_RANDOMIZED) + { + z2Hash = Util.ToGameText("VER. 2.0.0 "); + } ROMData.Put(0x17C2C, z2Hash); Hash = Util.FromGameText(z2Hash); @@ -556,7 +567,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) { @@ -568,7 +579,7 @@ public async Task Randomize(byte[] vanillaRomData, RandomizerC private Assembler CreateAssemblyEngine() { - var asm = NewAssembler(assemblerOptions); + var asm = NewAssembler(assemblerOptions, false); asm.Callbacks = new Js65Callbacks { OnFileReadText = AsmFileReadTextCallback, @@ -1369,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; } @@ -2416,46 +2427,61 @@ 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); - rom.Put(ROM.ChrRomOffset + 0x01000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.randomizer_text.chr")); - - if (props.EncounterRates == EncounterRate.NONE) + if (!props.MarioMode) { - rom.Put(0x294, 0x60); //skips the whole routine + // 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); } - - if (props.EncounterRates == EncounterRate.HALF) + if (IS_RANDOMIZED) + rom.Put(ROM.ChrRomOffset + 0x01000, Util.ReadBinaryResource("Z2Randomizer.RandomizerCore.Asm.Graphics.randomizer_text.chr")); + else { - //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 }); + // 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 + 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[..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++) + { + 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")); + 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]); + // 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); + // 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) + { + rom.Put(0x294, 0x60); //skips the whole routine + } if (props.ShuffleLifeRefill) { @@ -2744,12 +2770,9 @@ what is visible in the dark and also doesn't add enough to be worth it imo. 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]); - 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.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]); foreach (Location location in pbagHearts) { @@ -3314,6 +3337,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"); } @@ -3613,17 +3637,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 @@ -3678,6 +3698,81 @@ 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 + +.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 + ; 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 + .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); @@ -3728,10 +3823,14 @@ 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); + 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"); } - public void AddCredits(Assembler asm) + public void AddCredits(Assembler asm, RandomizerProperties props) { byte[] CmdText(int x, int y, string text) { @@ -3759,18 +3858,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 @@ -3778,21 +3925,32 @@ .word NewCreditsBody """, "add_credits.s"); } - private void ChangeMapperToMMC5(Assembler asm, bool preventFlash, bool enableZ2Ft) + + private void MarioModeActivate(Assembler asm) { - var a = asm.Module(); - a.Assign("PREVENT_HUD_FLASH_ON_LAG", preventFlash ? 1 : 0); - a.Assign("ENABLE_Z2FT", enableZ2Ft ? 1 : 0); - AssignRealPalaceLocations(a); - a.Code(Util.ReadResource("Z2Randomizer.RandomizerCore.Asm.MMC5.s"), "mmc5_conversion.s"); + // "metasprite_engine.s" is included in metasprite.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(); + 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 r, List texts, ROM rom, StatRandomizer randomizedStats) + private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Random RNG, List texts, ROM rom, StatRandomizer randomizedStats) { bool randomizeMusic = !props.DisableMusic && props.RandomizeMusic; - ChangeMapperToMMC5(engine, props.DisableHUDLag, randomizeMusic); // will make output vary with customize tab options - rom.AddRandomizerToTitle(engine); + 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, IS_RANDOMIZED); AddCropGuideBoxesToFileSelect(engine); FixHelmetheadBossRoom(engine); FullItemShuffle(engine, GetNonSideviewItemLocations()); @@ -3807,7 +3965,12 @@ 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) + { + MarioModeActivate(engine); + } if (props.ShuffleBossHP != EnemyLifeOption.VANILLA) { @@ -3845,6 +4008,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); @@ -3880,6 +4048,11 @@ private void ApplyAsmPatches(RandomizerProperties props, Assembler engine, Rando rom.DashSpell(engine); } + if (props.FluteWarpMode != FluteWarpMode.NONE) + { + ROM.FluteTwisterWarp(engine, props.FluteWarpMode); + } + if (props.UpARestartsAtPalaces) { RestartWithPalaceUpA(engine); @@ -3920,11 +4093,49 @@ 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; + const byte LDX_ABS = 0xAE; + const byte STX_ABS = 0x8E; + + 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); + 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"); + + 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); + } } + UpdateMovingNpcDialog(engine, CustomTexts.BuildMovingNpcDialogPool(props)); UpdateTexts(engine, texts); } diff --git a/RandomizerCore/Music.cs b/RandomizerCore/Music.cs index 7dd572ba1..8f1cbf00c 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; @@ -28,6 +29,9 @@ internal enum Usage Boss, LastBoss, Credits, + GameComplete, + House, + PalaceClear, } /*internal class LoggerAdapter : Logger @@ -97,9 +101,12 @@ 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(0xc, "Palace Clear", [Usage.PalaceClear]), new(0xd, "Great Palace", [Usage.GreatPalace]), + new(0xf, "Game Complete", [Usage.GameComplete]), new(0x10, "Credits", [Usage.Credits]), new(0x12, "Last Boss", [Usage.LastBoss]), }; @@ -201,12 +208,28 @@ public void ImportSongs() { Usage.Boss, 1 }, { Usage.LastBoss, 1 }, { Usage.Credits, 1 }, + { Usage.GameComplete, 1 }, + { Usage.House, 1 }, + { Usage.PalaceClear, 1 }, }; var selUsesSongs = _imptr.SelectUsesSongs( usesSongs, numUsageSongs, _shuffler); WriteLogLine(null); WriteLogLine("SELECTED SONGS:"); + // I want my songs in a particular order for z2mario, so uncomment this when making the romhack + 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"))!; + selUsesSongs[Usage.Encounter] = + [ + regular, + regular, + swing, + swing, + ]; + } foreach (var (usage, usageSongs) in selUsesSongs) { string songNames = string.Join("}, {", usageSongs); @@ -237,7 +260,9 @@ public void ImportSongs() void WriteLogLine(string? line) { - Trace.WriteLine(line); +#if DEBUG + Console.WriteLine(line); +#endif _spoilerSb.AppendLine(line); } @@ -307,9 +332,12 @@ SongMap CreateSongMap(UsesSongs usesSongs) { { Usage.Title, 0 }, { Usage.Overworld, 1 }, + { Usage.House, 7 }, { Usage.Boss, 0xb }, + { Usage.PalaceClear, 0xc }, { Usage.LastBoss, 0x12 }, { Usage.Credits, 0x10 }, + { Usage.GameComplete, 0xf }, }; SongMap songMap = new(); diff --git a/RandomizerCore/OutputFilenameFormatter.cs b/RandomizerCore/OutputFilenameFormatter.cs new file mode 100644 index 000000000..09631d201 --- /dev/null +++ b/RandomizerCore/OutputFilenameFormatter.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Z2Randomizer.RandomizerCore; + +public static class OutputFilenameFormatter +{ + public const string DefaultTemplate = "Z2-%f-%s-%h.nes"; + + // The characters Windows forbids in a filename. Linux and macOS only + // disallow '/' (and NUL), so honoring the Windows set produces a name that + // is valid on all three platforms. + private static readonly char[] InvalidFileNameChars = + ['<', '>', ':', '"', '/', '\\', '|', '?', '*']; + + public static string Format(string? template, string flags, string? seed, string hash, DateTime? timestamp = null, string? version = null) + { + var resolvedTemplate = string.IsNullOrWhiteSpace(template) ? DefaultTemplate : template; + var resolvedTimestamp = (timestamp ?? DateTime.Now).ToString("yyyy-MM-dd-HHmm", CultureInfo.InvariantCulture); + var normalizedHash = string.Concat((hash ?? "").Where(c => !char.IsWhiteSpace(c))); + + var formatted = resolvedTemplate + .Replace("%d", resolvedTimestamp, StringComparison.Ordinal) + .Replace("%f", flags, StringComparison.Ordinal) + .Replace("%s", seed ?? "", StringComparison.Ordinal) + .Replace("%h", normalizedHash, StringComparison.Ordinal) + .Replace("%v", version ?? "", StringComparison.Ordinal); + + return SanitizeFileName(formatted); + } + + // Replaces any character that wouldn't be a valid filename on Windows, + // Linux, or macOS so a user-supplied template can never produce a name the + // OS rejects. + public static string SanitizeFileName(string fileName) + { + var builder = new StringBuilder(fileName.Length); + foreach (var character in fileName) + { + var invalid = character < ' ' || Array.IndexOf(InvalidFileNameChars, character) >= 0; + builder.Append(invalid ? '_' : character); + } + + // Windows rejects names that end in a space or a dot. + var sanitized = builder.ToString().TrimEnd(' ', '.'); + if (sanitized.Length == 0) + { + return "_"; + } + + var dot = sanitized.IndexOf('.'); + var baseName = dot >= 0 ? sanitized[..dot] : sanitized; + + return sanitized; + } +} diff --git a/RandomizerCore/ROM.cs b/RandomizerCore/ROM.cs index 6ef10d972..63cccec33 100644 --- a/RandomizerCore/ROM.cs +++ b/RandomizerCore/ROM.cs @@ -434,11 +434,147 @@ public void WritePalacePalettes(List bricks, List curtains, List (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) @@ -924,7 +1048,7 @@ jmp LoadNewLevelCap nop LoadNewLevelCapReturn: ; $9F7F -.org $A89E +.reloc LoadNewLevelCap: lda $0777,X ; the instruction we overwrote with jmp cmp LevelCaps,X @@ -990,12 +1114,13 @@ public void UseExtendedBanksForPalaceRooms(Assembler a) ; move the pointers for the data for the sideviews to load from RAM instead. .segment "PRG7" .org $C50C ; patch where the pointer for sideview data is loaded - jsr CopySideviewIntoRAMAndLoadPointer + jsr LoadPointerForReadingBankedSideView + jsr CopySideviewIntoRAM jmp $C53A FREE_UNTIL $C53A .reloc -CopySideviewIntoRAMAndLoadPointer: +LoadPointerForReadingBankedSideView: ; y is the offset for the type to load from. ; 0 = encounter ; 1 = west hyrule town @@ -1035,6 +1160,10 @@ lda MapNumber sta $d5 lda ($02),y sta $d7 + rts + +.reloc +CopySideviewIntoRAM: ; if its not a palace area, skip switching banks lda WorldNumber @@ -1168,6 +1297,7 @@ public void UpdateMapPointers() public void UpAController1(Assembler a) { a.Module().Code(""" +.include "z2r.inc" .segment "PRG0" .org $a19f CheckController1ForUpAUnknown: @@ -1192,6 +1322,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) @@ -1872,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, @@ -1914,8 +2079,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 @@ -1956,7 +2119,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 @@ -2037,6 +2200,50 @@ public void AggressiveThunderbird() Put(0x16413, thunderBirdHP); } + 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(); @@ -2282,6 +2489,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); @@ -2648,4 +2858,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)."); + } + } } diff --git a/RandomizerCore/RandomizerConfiguration.cs b/RandomizerCore/RandomizerConfiguration.cs index 9853cc6f5..faa268054 100644 --- a/RandomizerCore/RandomizerConfiguration.cs +++ b/RandomizerCore/RandomizerConfiguration.cs @@ -681,6 +681,12 @@ private bool palaceStylesAnyMetastyleSelected() [Reactive] private bool permanentBeamSword; + [Reactive] + private bool marioMode; + + [Reactive] + private FluteWarpMode fluteWarpMode; + //Custom [Reactive] [IgnoreInFlags] @@ -734,6 +740,10 @@ private bool palaceStylesAnyMetastyleSelected() [IgnoreInFlags] private string spriteName; + [Reactive] + [IgnoreInFlags] + private string outputFilenameTemplate = OutputFilenameFormatter.DefaultTemplate; + [Reactive] [IgnoreInFlags] @@ -840,6 +850,7 @@ public RandomizerConfiguration() fastSpellCasting = false; shuffleSpritePalettes = false; permanentBeamSword = false; + marioMode = false; upAOnController1 = false; removeFlashing = true; sprite = CharacterSprite.LINK; @@ -903,9 +914,9 @@ public static void SerializeInt(FlagBuilder flags, string name, int? val, int mi // For nullable ints, Enums are our preferred option. int extent = maximum - minimum + 1; int value = val - minimum ?? minimum; - if (value < minimum || value > maximum) + if (value < 0 || value >= extent) { - logger.Warn($"Property ({name}={value}) is out of range."); + logger.Warn($"Property ({name}={val}) is out of range."); } flags.Append(value, extent); } @@ -1065,6 +1076,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 @@ -1453,6 +1465,7 @@ public RandomizerProperties Export(Random r, bool includeDifficulty = true) }; properties.JumpAlwaysOn = jumpAlwaysOn; properties.DashAlwaysOn = dashAlwaysOn; + properties.FluteWarpMode = fluteWarpMode; properties.FastCast = fastSpellCasting; properties.BeamSprite = beamSprite; properties.DisableMusic = disableMusic; diff --git a/RandomizerCore/RandomizerCore.csproj b/RandomizerCore/RandomizerCore.csproj index afe3c142d..95d4601c3 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,15 +56,40 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/RandomizerCore/RandomizerProperties.cs b/RandomizerCore/RandomizerProperties.cs index 271d0d85d..3e93f3e51 100644 --- a/RandomizerCore/RandomizerProperties.cs +++ b/RandomizerCore/RandomizerProperties.cs @@ -214,10 +214,12 @@ 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; } public bool DashAlwaysOn { get; set; } + public FluteWarpMode FluteWarpMode { get; set; } public bool FastCast { get; set; } public BeamSprites BeamSprite { get; set; } public bool DisableMusic { get; set; } diff --git a/RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs b/RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs index 2378a7e6c..33ecc7402 100644 --- a/RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs +++ b/RandomizerCore/Sidescroll/ByEntranceDirectionItemRoomSelectionStrategy.cs @@ -23,10 +23,12 @@ public override Room[] SelectItemRooms(Palace palace, RoomPool roomPool, int ite if (!roomPool.ItemRoomsByDirection.TryGetValue(itemRoomDirection, out var value)) { remainingDirections.Remove(itemRoomDirection); +#pragma warning disable CS0162 // Unreachable code detected if (BREAK_EARLY_ON_FIRST_FAILED_DIRECTION) { return []; } +#pragma warning restore CS0162 // Unreachable code detected continue; } Room itemRoom = value.Next(r); diff --git a/RandomizerCore/Sidescroll/RoomPool.cs b/RandomizerCore/Sidescroll/RoomPool.cs index d94e23eff..2bddd9d39 100644 --- a/RandomizerCore/Sidescroll/RoomPool.cs +++ b/RandomizerCore/Sidescroll/RoomPool.cs @@ -246,9 +246,9 @@ public void RemoveDuplicates(RandomizerProperties props, Room roomThatWasUsed) { //Default stubs are used when no deadend rooms of that direction exist. They are always allowed to //be duplicates and should never be removed from the pool. - if (DefaultStubsByDirection.TryGetValue(roomThatWasUsed.CategorizeExits(), out Room directionStub)) + if (DefaultStubsByDirection.TryGetValue(roomThatWasUsed.CategorizeExits(), out Room? directionStub)) { - if(directionStub == roomThatWasUsed) + if (directionStub == roomThatWasUsed) { return; } diff --git a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs b/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs index 8f9ea26c9..28c1d7d43 100644 --- a/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs +++ b/RandomizerCore/Sidescroll/ShapeFirstCoordinatePalaceGenerator.cs @@ -304,7 +304,7 @@ protected virtual bool ValidateShape(Palace palace, Dictionary walkGraph, bool includeCoordinateGrid = true, List prepopulatedCoordinates = null) + public static string GetLayoutDebug(Dictionary walkGraph, bool includeCoordinateGrid = true, List? prepopulatedCoordinates = null) { StringBuilder sb = new(); if (includeCoordinateGrid) diff --git a/RandomizerCore/StatRandomizer.cs b/RandomizerCore/StatRandomizer.cs index c3e6f0864..c931968fb 100644 --- a/RandomizerCore/StatRandomizer.cs +++ b/RandomizerCore/StatRandomizer.cs @@ -239,6 +239,11 @@ protected void WriteEnemyStats(ROM rom) protected static int[] RandomizeExperienceToLevel(int[] baseExpTable, Random r, bool[] shuffleStat, int[] levelCap, bool scaleLevels) { + if (!shuffleStat[0] && !shuffleStat[1] && !shuffleStat[2] && levelCap[0] == 8 && levelCap[1] == 8 && levelCap[2] == 8) + { + return baseExpTable; + } + const int STAT_COUNT = 3; const int LEVELS_PER_STAT = 8; const int TABLE_SIZE = STAT_COUNT * LEVELS_PER_STAT; 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/FlagsTests.cs b/Tests/FlagsTests.cs index af39ddb85..2ef5017a9 100644 --- a/Tests/FlagsTests.cs +++ b/Tests/FlagsTests.cs @@ -2,6 +2,7 @@ using CrossPlatformUI.Presets; using Z2Randomizer.RandomizerCore; using Z2Randomizer.RandomizerCore.Flags; +using Random = Z2Randomizer.RandomizerCore.Random; namespace Z2Randomizer.Tests; @@ -237,6 +238,98 @@ public void ShareSeedAcrossDifficultyRoundTripsInFlags() Assert.IsTrue(config2.ShareSeedAcrossDifficulty); } + + [TestMethod] + public void FlagPasteParserExtractsFlagsAndLongestSeedFromMessage() + { + var flags = StandardPreset.Preset.SerializeFlags(); + var input = $"race info, seed 1234. use {flags}, tracking 001234567."; + + Assert.AreEqual(flags, FlagPasteParser.ExtractFlags(input)); + Assert.AreEqual("001234567", FlagPasteParser.ExtractSeed(input)); + } + + [TestMethod] + public void FlagPasteParserRejectsInvalidCharacters() + { + Assert.IsFalse(FlagPasteParser.IsValidFlagString("not-a-flag-string")); + } + + [TestMethod] + public void FlagPasteParserExtractsFlagsFromSeedAndFlagsMessage() + { + const string flags = "hEAAdgKyAs6WvqAssLJidnf7xJOY+0csos1gnXRs!AAFR+h"; + var input = $"Seed: 228401255 - Flags: {flags}"; + + Assert.AreEqual(flags, FlagPasteParser.ExtractFlags(input)); + Assert.AreEqual("228401255", FlagPasteParser.ExtractSeed(input)); + } + + [TestMethod] + public void FlagPasteParserExtractsFlagsFromEmbeddedLabels() + { + const string flags = "hEAAdgKyAs6WvqAssLJidnf7xJOY+0csos1gnXRs!AAFR+h"; + var input = $"Seed:228401255-Flags:{flags}"; + + Assert.AreEqual(flags, FlagPasteParser.ExtractFlags(input)); + Assert.AreEqual("228401255", FlagPasteParser.ExtractSeed(input)); + } + + [TestMethod] + public void FlagPasteParserDoesNotExtractSeedFromPlainFlagString() + { + const string flags = "hEAAdgKyAs6WvqAssLJidnf7xJOY+0csos1gnXRs!AAFR+h"; + + Assert.AreEqual(flags, FlagPasteParser.ExtractFlags(flags)); + Assert.IsNull(FlagPasteParser.ExtractSeed(flags)); + } + + [TestMethod] + public void FlagPasteParserRequiresDelimitedSixDigitSeedToken() + { + const string flags = "hEAAdgKyAs6WvqAssLJidnf7xJOY+0csos1gnXRs!AAFR+h"; + + Assert.IsNull(FlagPasteParser.ExtractSeed($"Seed: 12345 - Flags: {flags}")); + Assert.AreEqual("228401255", FlagPasteParser.ExtractSeed($"Seed: 228401255 - Flags: {flags}")); + } + + [TestMethod] + public void OutputFilenameTemplateDefaultsAndReplacesTokens() + { + var timestamp = new DateTime(2026, 5, 31, 19, 53, 3); + + var formatted = OutputFilenameFormatter.Format("%d-%f-%s-%h.nes", "FLAGS", "0123", "HA SH", timestamp); + + Assert.AreEqual("2026-05-31-1953-FLAGS-0123-HASH.nes", formatted); + Assert.AreEqual("Z2-FLAGS-0123-HASH.nes", OutputFilenameFormatter.Format("", "FLAGS", "0123", "HASH", timestamp)); + } + + [TestMethod] + public void OutputFilenameSanitizesCharactersInvalidOnAnyPlatform() + { + var formatted = OutputFilenameFormatter.Format("%f:%s?.nes", "FL/AGS", "0*123", "HASH"); + + Assert.AreEqual("FL_AGS_0_123_.nes", formatted); + } + + [TestMethod] + public void OutputFilenameTemplateReplacesVersionToken() + { + var formatted = OutputFilenameFormatter.Format("Z2-%v-%s.nes", "FLAGS", "0123", "HASH", version: "5.1.4"); + + Assert.AreEqual("Z2-5.1.4-0123.nes", formatted); + } + + [TestMethod] + public void OutputFilenameTemplateIsIgnoredByFlags() + { + RandomizerConfiguration config = new(); + var originalFlags = config.SerializeFlags(); + + config.OutputFilenameTemplate = "custom-%s-%h.nes"; + + Assert.AreEqual(originalFlags, config.SerializeFlags()); + } public void TestSgl2025EncodeCycle() { 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 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}"