diff --git a/README.md b/README.md index 4b10236e..dda6b4ee 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ The rest are previews and dumps, each driving a real screen or table: ```bash # the full walked route: Johto, the Hall of Fame, every Kanto gym, and Red godot --headless --path . -s res://tools/preview_world_story.gd -- crystal 24 7 2 2 1 none home story -# Red, Blue or Yellow from the bedroom to Lt. Surge +# Red, Blue or Yellow from the bedroom to the Hall of Fame godot --headless --path . -s res://tools/preview_world_story.gd -- red 0 38 3 6 1 none home story ``` diff --git a/autoload/diagnostics.gd b/autoload/diagnostics.gd index ad16f65c..096e9e20 100644 --- a/autoload/diagnostics.gd +++ b/autoload/diagnostics.gd @@ -3,7 +3,6 @@ extends Node ## What a player hands over when something goes wrong: this build, this machine, ## the settings and mods in force, and the engine's own log of the session. -## ## A sink rather than a set of call sites: [Logger] goes in through ## [method OS.add_logger], so every print, warning and runtime error in the game, ## a tool or a mod reaches the report without being routed twice. The file at @@ -62,7 +61,6 @@ static func instance() -> Gen2Diagnostics: ## Records one line of context. Safe before the autoload exists and in a tool ## run, so a caller never guards the call itself. -## ## Goes through `print`, which is what puts it in the engine's log file next to ## the errors it explains; the sink below picks it up on the way past. static func note(topic: String, message: String) -> void: @@ -92,7 +90,6 @@ func _ready() -> void: ## Printed rather than written, so it is near the top of the engine's own log ## file: a crash log is then self-describing whether or not the player ever ## reaches the launcher again. -## ## A player's launch only, for the reason [method trace] gives: a check or a ## tool is read for the answer it prints, and a header no one asked for is in ## the way of it. @@ -209,7 +206,6 @@ func report() -> String: ## Writes the report and every kept log file into one `.zip` under ## [param folder], and answers the path it wrote. -## ## A zip rather than the log itself because the useful thing is the whole set: ## the session that crashed is usually the file *before* the one this launch is ## writing. An empty [param folder] takes the platform's downloads directory, @@ -285,7 +281,6 @@ func log_files(directory: String = DIRECTORY) -> PackedStringArray: ## Drops log files in [param directory] past the count, the age or the total ## size this keeps, whichever bites first, oldest first. -## ## The engine's own rotation counts files at startup and nothing else, so a ## single session that logged for a week, or a build that once wrote under ## another name, would otherwise sit in the player's data directory for good. @@ -339,7 +334,6 @@ func record(level: String, message: String) -> void: ## Reads the previous session's marker and raises this one's. -## ## Only a player's own launch writes it. A headless check or a `-s` tool that ## the wall-clock cap kills never reaches [method _exit_tree], so letting those ## write the marker would report a crash to the player at the next launch, and @@ -510,7 +504,6 @@ static func _video_adapter() -> String: ## The sink itself, kept beside the autoload rather than in a file of its own: ## it is four lines and it has no other caller. -## ## Nothing here prints. A logger that raised a message of its own would be ## handed it straight back. class Gen2DiagnosticsSink extends Logger: diff --git a/autoload/game_runtime.gd b/autoload/game_runtime.gd index 070647b6..fe8d2db7 100644 --- a/autoload/game_runtime.gd +++ b/autoload/game_runtime.gd @@ -2,7 +2,6 @@ class_name Gen2GameRuntime extends Node ## Runtime selection shared by the launcher and screens opened from it. -## ## Cartridge bytes and decoded data stay owned by their existing layers. The ## selected save is the exception: it is mutable, and every screen has to see the ## same instance. Re-reading the slot per call handed a battle result, a party @@ -14,7 +13,6 @@ var selected_game_id: StringName = &"" var selected_save_slot: int = -1 ## The new game the intro is running for, before it exists on disk. -## ## `NewGame` reaches `InitializeWorld` only after `PlayerProfileSetup` and ## `OakSpeech` have both returned, so there is nothing to write until the intro ## finishes. The launcher stages the slot and the slot's own label here, the @@ -99,7 +97,6 @@ var _second_screen_host: Gen2SecondScreenHost = null ## Loads installed mods before any screen exists. -## ## A mod registers what it provides and returns, so this must happen before the ## first screen asks the host: a renderer registered after the overworld was ## built would not be offered until the next map. It also means a broken mod is diff --git a/game/battle/accuracy.gd b/game/battle/accuracy.gd index a8aaad9d..d6115bdc 100644 --- a/game/battle/accuracy.gd +++ b/game/battle/accuracy.gd @@ -2,11 +2,9 @@ class_name Gen2Accuracy extends RefCounted ## Whether a move connects. -## ## Accuracy is a byte out of 255, not a percentage: "100%" stores 255 and "90%" ## stores 229. Kept as the byte, because the roll is against it and a stored 255 ## is a special case that never misses. -## ## Accuracy and evasion use their own multiplier table, a different shape from ## the other stats', so -1 accuracy and -1 Attack are different fractions. @@ -23,10 +21,8 @@ const STAGE_MULTIPLIERS: Array = [ ## The chance a move connects, out of 255. -## ## Evasion reads the same table from the other end rather than a second table: ## +2 evasion is the multiplier accuracy uses at -2. -## ## [param foresight] drops both sides' stages, not only evasion, and only when ## the evasion stage is at least the accuracy stage, so it cannot undo an ## accuracy the attacker raised. @@ -58,7 +54,6 @@ static func apply_stage(value: int, stage: int) -> int: ## Rolls a hit against a chance out of 255. -## ## A chance of exactly 255 connects without rolling. Rolling would miss one time ## in 256, and Crystal goes out of its way not to. [param generation] is ## `MoveHitTest`, which does not: pret's own comment there says even the highest diff --git a/game/battle/ai.gd b/game/battle/ai.gd index be0dea9f..545ac974 100644 --- a/game/battle/ai.gd +++ b/game/battle/ai.gd @@ -2,7 +2,6 @@ class_name Gen2BattleAI extends RefCounted ## Scores an enemy trainer's move choice the way the cartridge's own AI does. -## ## Every slot starts at 20, or 80 with no PP. Each bit set in the trainer class's ## [constant Gen2Layout.ATTR_AI_MOVE_WEIGHTS] runs one scoring layer over the four ## slots, nudging scores up (discourage) or down (encourage); lowest wins, ties @@ -1259,7 +1258,6 @@ static func _smart_pursuit(scores: Array, slot: int, c: Context) -> void: ## `AI_Smart_Protect`: one ladder of tests, first match winning. -## static func _smart_protect(scores: Array, slot: int, c: Context) -> void: if c.attacker.protect_count != 0: _smart_protect_discourage(scores, slot, c, true) @@ -1398,7 +1396,6 @@ static func _apply_opportunist(scores: Array, c: Context) -> void: ## whichever deals the most, unless it would cost the mon itself ## ([constant RECKLESS_EFFECTS]) or does one point of damage that is really a ## fixed-damage move ([code]power < 2[/code]). -## ## The estimate is [method _estimate_damage], which is `AIDamageCalc` itself. static func _apply_aggressive(scores: Array, c: Context) -> void: var attacker: Gen2BattleMon = c.attacker @@ -1450,7 +1447,6 @@ static func _estimate_damage(c: Context, move: Dictionary, constant: bool = true ## [constant Gen2Layout.AI_CAUTIOUS]: discourage [constant RESIDUAL_MOVE_NUMBERS] ## once it is no longer the attacker's first turn. -## ## Diverges from a documented source bug (`docs/bugs_and_glitches.md`, ## "'Cautious' AI may fail to discourage residual moves") unless ## `cautious_ai_abandons_remaining_moves` is on: `ret nc` abandons the remaining diff --git a/game/battle/ai_switch.gd b/game/battle/ai_switch.gd index 6100a2f6..1522dd32 100644 --- a/game/battle/ai_switch.gd +++ b/game/battle/ai_switch.gd @@ -4,7 +4,6 @@ extends RefCounted ## Whether a trainer pulls its Pokemon out, and which one it sends instead: ## `CheckAbleToSwitch` and its five party scans in `engine/battle/ai/switch.asm`, ## plus the three frequency gates in `items.asm`. -## ## The cartridge works in six-bit masks and reuses one variable as both a score ## and a party index; here the masks are arrays of indices and the two uses are ## separate return values. Nothing changes: every scan keeps party order, and @@ -51,7 +50,6 @@ const THREAT_LOW: int = 5 ## Whether this trainer switches, and to which party index. -## ## Answers [code]{"switch": bool, "index": int}[/code]. [param flags] is the ## class's own [constant Gen2Layout.ATTR_AI_ITEM_SWITCH] word; a class with none ## of the three switch bits never switches, which is `AI_SwitchOrTryItem`'s own @@ -75,7 +73,6 @@ static func decide(battle: Gen2Battle, flags: int, rng: RandomNumberGenerator) - ## `FindMonInOTPartyToSwitchIntoBattle`: who the AI would rather have in, with no ## opinion about whether it should switch at all. -## ## [method decide] is the ordinary route and answers both questions at once. ## Baton Pass is the one caller that has already settled the first, so it needs ## the pick on its own. Nobody standing answers -1; a shortlist that resists @@ -171,7 +168,6 @@ static func _counter_choice(battle: Gen2Battle, immune: Array) -> Dictionary: ## `CheckPlayerMoveTypeMatchups`: how well the Pokémon that is out is doing, ## starting at [constant BASE_SCORE]. Higher is better for staying in. -## ## The first half reads what the player has actually thrown so far, falling back ## to the player's own types when it has thrown nothing. The second half reads ## what the enemy's own moves would do back. @@ -296,7 +292,6 @@ static func _alive_enough_to_switch(battle: Gen2Battle) -> bool: ## `FindEnemyMonsWithAtLeastQuarterMaxHP`, narrowing whoever was passed in. -## ## The name is the only quarter in it. `AICheckEnemyQuarterHP` doubles the HP ## word twice with `sla c / rl b`; this routine writes `srl c / rl b`, so the low ## byte is halved twice while the high byte is doubled twice and picks up the two @@ -313,7 +308,6 @@ static func _at_least_quarter_hp(battle: Gen2Battle, candidates: Array) -> Array ## The `srl c / rl b` pair run twice over the HP word, as a number. -## ## For a maximum HP under 256 the whole thing comes to "current HP is not a ## multiple of four": the high byte ends up holding the two bits the low byte ## dropped, so anything but a multiple of four lands over 255 and clears any @@ -364,7 +358,6 @@ static func _resisting(battle: Gen2Battle, candidates: Array) -> Array: ## `FindEnemyMonsWithASuperEffectiveMove`, with `FindAliveEnemyMons` folded in ## the way `FindAliveEnemyMonsWithASuperEffectiveMove` folds it. -## ## Answers [code]{"index": int, "quality": int}[/code]: quality 2 for the first ## candidate holding a move that is super effective against whoever the player ## has out, 1 for the first that at least has a neutral one, and 0 for nobody. diff --git a/game/battle/battle.gd b/game/battle/battle.gd index e9d4fd44..120224e7 100644 --- a/game/battle/battle.gd +++ b/game/battle/battle.gd @@ -3,7 +3,6 @@ extends RefCounted ## A battle: two parties, a turn at a time. Scene-free with randomness injected, ## so a whole battle can be fought in a test with no display. -## ## A turn answers with events carrying their own numbers, never a string: ## sentences, animation and draining bars are the screen's job. A side is a party ## and a wild encounter a party of one. A turn ending with somebody down says so @@ -751,7 +750,6 @@ static func use_item(item: int) -> Dictionary: ## `InitEnemyTrainer`: the class's own `TRNATTR_ITEM1` and `TRNATTR_ITEM2` into ## the two working slots, and then `IsGymLeader`'s own party walk. `NO_ITEM` is ## zero and is not carried. -## ## [param rewarded] is false for the two opponents `ReadTrainerParty` returns in ## front of, the Battle Tower's and a link partner's: neither reaches ## `ComputeTrainerReward`, and neither win branch pays money either. @@ -1513,12 +1511,10 @@ static func double_reward(amount: int) -> int: ## `BattleWon.give_money`, the whole of what a beaten trainer pays. -## ## [param reward] is [member battle_reward], a quarter of the prize: the Amulet ## Coin doubles that quarter, four quarters are handed out one at a time, and ## the two `.DoubleReward` calls at `.done` put the total back for the line that ## announces it. [param mom_flags] is `wMomSavingMoney`. -## ## Returns the two credits, the figure the line prints and which line it is. static func prize_money_split( reward: int, amulet: bool, mom_flags: int, moms_money: int, max_money: int @@ -1565,7 +1561,6 @@ func _send_out_animation(enemy_turn: bool, param: int) -> Dictionary: ## `SendOutMonText`, which picks one of four lines off how much of the opponent ## is left. Only the player is ever announced this way; the enemy has one line. -## ## The arithmetic is the source's own: the remaining HP times 25 over the top ## quarter of the maximum, both read as the cartridge reads them, so the answer ## is a percentage that has been through an eight-bit divisor. A maximum below @@ -2972,7 +2967,6 @@ func _act(side: int, slot: int, move_number: int, events: Array) -> void: ## The command interpreter: `DoMove`'s own read cycle over the list an effect ## byte picks, with `SkipToBattleCommand` and `endloop`'s rewind to `critical`. -## ## `ResetTurn`, used by Metronome, Mirror Move and Sleep Talk: the called move ## replaces the working one and starts its list from the beginning, without the ## once-per-action status gate. A fresh [Gen2Turn] is that clean move-struct copy, @@ -3035,7 +3029,6 @@ func run_move_effect(turn: Gen2Turn, depth: int = 0) -> void: ## calls, `RegionCheck` and `IsGymLeader`. Kept here rather than on the screen ## because every input is battle state: `wBattleType`, `wOtherTrainerClass`, ## `wOtherTrainerID`, `wTimeOfDay` and the map's landmark. -## ## `MUSIC_SUICUNE_BATTLE` exists on Crystal alone; the two `BATTLETYPE_` rows in ## front of the trainer check are the only place either game reaches it, and ## Gold and Silver never write those types. @@ -3112,7 +3105,6 @@ const LANDMARK_VICTORY_ROAD: int = 0x58 ## does everything below `KANTO_LANDMARK`, and so does Victory Road and every ## landmark above it, because `cp LANDMARK_VICTORY_ROAD / jr c, .kanto` only ## takes the Kanto branch below that row. -## ## The `LANDMARK_SPECIAL` backup lookup in front of it is ## [method Gen2WorldAPI.landmark_backup], which every caller of this resolves ## the landmark through. @@ -3125,7 +3117,6 @@ static func region_is_kanto(landmark_id: int, crystal: bool = true) -> bool: ## `PlayBattleMusic`'s answer: the track a battle opens on. -## ## [param landmark_id] is `GetWorldMapLocation`'s, [param trainer_class] and ## [param trainer_id] are `wOtherTrainerClass` and `wOtherTrainerID` (class 0 ## being a wild fight), and [param day_period] is `wTimeOfDay`. diff --git a/game/battle/battle_anim_bg_effects.gd b/game/battle/battle_anim_bg_effects.gd index 658ea4e7..776c4b89 100644 --- a/game/battle/battle_anim_bg_effects.gd +++ b/game/battle/battle_anim_bg_effects.gd @@ -4,7 +4,6 @@ extends RefCounted ## The `BATTLE_BG_EFFECT_*` jumptable and the routines it dispatches ## (engine/battle_anims/bg_effects.asm): the screen shakes, the scanline ## deformations, the palette fades and the tilemap edits. Five run at once. -## ## An effect id is profile-local and is never normalised: pokegold ships no ## `BATTLE_BG_EFFECT_BODY_SLAM`, so every id from $25 on names a different effect ## in the two games; both jumptables are kept whole and dispatch is by name. The @@ -444,7 +443,6 @@ static func _hues( ## `BGEffect_RapidCyclePals`, the Color branch: one side of the field at a time, ## and the enemy's own states are the two above the player's. -## ## Nothing here ends the walk. The list's own `$ff` only stalls it; the effect is ## retired by the script's `anim_incbgeffect`, which is what steps it onto the ## state that puts the palette back. @@ -1336,7 +1334,6 @@ static func _shake_amount( ## `BattleBGEffect_Rollout`: the screen shaken vertically, with the first ## animation object moved the opposite way so the ball it draws stays put. -## ## The source waits a frame here rather than at the bottom of `.playframe`; ## in a player stepped once per frame that is the same one frame either way. static func _rollout( diff --git a/game/battle/battle_anim_object.gd b/game/battle/battle_anim_object.gd index 31ac7265..bfe65250 100644 --- a/game/battle/battle_anim_object.gd +++ b/game/battle/battle_anim_object.gd @@ -5,7 +5,6 @@ extends RefCounted ## (engine/battle_anims/core.asm, helpers.asm). `anim_obj` names a row of ## `BattleAnimObjects`, which supplies the frameset, the motion callback, the ## palette and the graphics sheet, and a place to put it. -## ## Every field is a cartridge byte: coordinates wrap at 256, and an object walking ## off one side is that wrap rather than a clamp. `frame` starts at -1 because ## `GetBattleAnimFrame` increments before it reads. @@ -108,11 +107,9 @@ func active() -> bool: ## `BattleAnimOAMUpdate`, as the sprites it would write into `wShadowOAM`. -## ## Returns [code]{ deleted, sprites }[/code]. `sprites` is empty for an ## `oamwait`, which is a frame the object is simply not drawn on, and for the ## `oamdelete` that ends it; `deleted` says which of the two happened. -## ## [param enemy_turn] is `hBattleTurn`, the only thing that decides whether the ## coordinates are mirrored onto the other side of the field. func oam_update( @@ -207,7 +204,6 @@ func _attributes(attributes: int, flags: int, object_palette: int) -> int: ## `GetBattleAnimFrame`. Answers the OAM set to draw, or one of the three ## commands, and leaves [member _frame_flags] set for the frame it chose. -## ## While a frame still has duration left it is redrawn without advancing. ## `oamend` steps back two and loops, which redraws the last real frame forever; ## `oamrestart` goes back to -1 and loops, which starts the frameset again. diff --git a/game/battle/battle_anim_script.gd b/game/battle/battle_anim_script.gd index a0e60cbd..0c0a896c 100644 --- a/game/battle/battle_anim_script.gd +++ b/game/battle/battle_anim_script.gd @@ -4,7 +4,6 @@ extends RefCounted ## The battle animation command interpreter (engine/battle_anims/anim_commands.asm). ## Nothing here draws, plays a sound or touches a palette: the commands are ## reported and whoever is drawing decides what they look like. -## ## Anything below [constant FIRST_COMMAND] is a delay rather than a command, so ## `anim_wait N` is exactly N frames. The control flow is the cartridge's single ## `wBattleAnimParent` word and single `wBattleAnimLoops` byte, so a call inside a @@ -116,7 +115,6 @@ const WOBBLE_CAUGHT: int = 1 const WOBBLE_ESCAPED: int = 2 ## How many commands one frame may run before the script is abandoned. -## ## The cartridge has no such limit: `.RunScript` loops until a delay byte or a ## top-level `anim_ret`, and a script that does neither hangs the hardware. This ## is ours, so a malformed cached region or a mod's own script costs a refused @@ -151,7 +149,6 @@ var _failed: bool = false ## An interpreter positioned at [param start_address] inside [param region], which ## starts at [param base_address]. -## ## [param param] is `wBattleAnimParam`, which the caller sets before playing; ## `wBattleAnimVar` starts at zero because `ClearBattleAnims` clears it. static func create( @@ -173,7 +170,6 @@ static func create( ## [code]{ ok, name, byte, operands, size, target }[/code]; `target` is the ## start_address a branch would take, or -1. A delay byte answers [constant WAIT] with ## its own value as its single operand. -## ## Shared with [Gen2BattleAnimImporter], which walks every body with it, so the ## vocabulary is stated once. static func decode_command( @@ -250,7 +246,6 @@ func variable() -> int: ## One hardware frame of `RunBattleAnimCommand`, as an Array of the commands it ## ran, each [code]{ name, byte, operands }[/code]. -## ## Empty while a delay is counting down, which is most frames, and empty once ## the script has stopped. A frame that runs commands ends either on the delay ## byte that set the next pause or on the `anim_ret` that ended the script, and diff --git a/game/battle/battle_renderer.gd b/game/battle/battle_renderer.gd index b14eacb0..9f522cea 100644 --- a/game/battle/battle_renderer.gd +++ b/game/battle/battle_renderer.gd @@ -4,7 +4,6 @@ extends Control ## Draws the battle field: two pics, two status panels, two HP bars, the exp bar ## and whatever an animation is putting over them. Call [method set_battle_data] ## once, then [method set_view] whenever the screen has new display values. -## ## The pics are drawn through `wTilemap` rather than placed at a corner, because ## that map is what an animation edits. Panels compose into one screen-sized index ## buffer with index 0 transparent. Every background layer is one plane, so the @@ -436,7 +435,6 @@ static func minimize_pixels( ## `GetSubstitutePic`: a blank box with four tiles of [param strip], the monster ## overworld sprite, copied into it. The doll wears whichever battler palette its ## box sits in, since nothing writes one for it. -## ## Static because it is the whole of the picture and takes no screen: a check ## sweeping three caches builds it the same way the renderer does. static func substitute_pixels( @@ -582,7 +580,6 @@ static func _append_animation( ## A battler pic's own palette, permuted by whatever DMG byte the animation's ## last `BattleAnimRequestPals` left on that palette slot. -## ## `CGB_BattleColors` reads `CheckShininess` on both sides, so the shiny palette ## is the picture's for the whole fight and not just the gold sweep the entrance ## plays over it. @@ -776,7 +773,6 @@ func _draw_hud_balls() -> void: ## `wShadowOAM` as the animation left it: up to forty sprites, each eight by ## eight, in the order they were written, so a later one draws over an earlier. -## ## Objects are not part of the background plane and take no scroll. Index 0 is ## transparent, which is what OAM's own colour 0 is. func _draw_sprites() -> void: @@ -850,7 +846,6 @@ func _blit_sprite(into: Image, sprite: Dictionary, backpic: bool = false) -> voi ## The eight pixels by eight of one animation tile, found through the window ## [method Gen2BattleAnimPlayer.tiles] describes, counted from ## `BATTLEANIM_BASE_TILE`. -## ## A window tile is either an imported sheet's or one of the battle's own two ## pictures, which is what `anim_battlergfx_1row` and `..._2row` put there so an ## effect can move a battler as objects. diff --git a/game/battle/battle_screen.gd b/game/battle/battle_screen.gd index aed0eec6..541b1cde 100644 --- a/game/battle/battle_screen.gd +++ b/game/battle/battle_screen.gd @@ -70,7 +70,6 @@ const INFLICTED: Dictionary = { ## `BattleCommand_Charge.UsedText` (data/text/common_2.asm), which picks its line ## by move number rather than by effect: Fly and Dig share an effect byte and do ## not share a sentence. -## ## The source's own `line` is a line break in a fixed-width box rather than part ## of the sentence, so these read as one flowing string the way every other ## message here does. @@ -938,18 +937,35 @@ func _tick_auto_input() -> void: if stage != _auto_input_stage: _auto_input_stage = stage _auto_input_polls = 0.0 - if stage == &"": + var streams: Dictionary = _auto_input_streams() + if not streams.has(stage): _auto_input = [] else: - _start_auto_input(DUDE_AUTO_INPUT[stage]) + _start_auto_input(streams[stage]) if _auto_input_index >= _auto_input.size(): return - _auto_input_polls += float(DUDE_POLLS_PER_FRAME.get(_auto_input_stage, 1.0)) + _auto_input_polls += 1.0 if _generation() == RomRegistry.GEN1 \ + else float(DUDE_POLLS_PER_FRAME.get(_auto_input_stage, 1.0)) while _auto_input_polls >= 1.0 and _auto_input_index < _auto_input.size(): _auto_input_polls -= 1.0 _spend_auto_input_poll() +## `DudeAutoInputs`, or the cursor `DisplayBattleMenu` and `DisplayListMenuIDLoop` +## draw themselves: FIGHT, ITEM, the one ball, each held `DelayFrames` long. +func _auto_input_streams() -> Dictionary: + if _generation() != RomRegistry.GEN1: + return DUDE_AUTO_INPUT + var frames: Array = Gen1Layout.TUTOR_FRAMES.get(_data.id, Gen1Layout.TUTOR_FRAMES[RomRegistry.RED]) + return { + &"menu": [ + [PokeButton.NONE, int(frames[0]) - 1], [PokeButton.DOWN, int(frames[1]) - 1], + [PokeButton.A, 0], + ], + &"pack": [[PokeButton.NONE, int(frames[2]) - 1], [PokeButton.A, 0]], + } + + func _spend_auto_input_poll() -> void: if _auto_input_delay > 0: _auto_input_delay -= 1 @@ -1252,10 +1268,15 @@ func _begin_world_battle(prepared: Dictionary, save: Gen2SaveData) -> void: ## The Dude's bag, not the player's, which is why the world hands none. func _set_up_dude_tutorial() -> void: + _stop_auto_input() + if _generation() == RomRegistry.GEN1: + var balls: Dictionary = {_tutor_ball(): int(Gen1Layout.TUTOR_BALLS.get(_data.id, 1))} + set_battle_pack([_tutor_ball()], balls) + set_capture_balls([_tutor_ball()], balls) + return ## `.DudeTutorial` forces TEXT_DELAY_MED over the player's own TEXT SPEED. if _box != null: _box.reveal_speed = 1.0 / (Gen2Options.FRAME_SECONDS * Gen2Options.TEXT_DELAY_MED) - _stop_auto_input() set_battle_pack(DUDE_PACK, DUDE_PACK_QUANTITIES) set_capture_balls([Gen2WorldPartyHost.ITEM_POKE_BALL], DUDE_PACK_QUANTITIES) @@ -1434,12 +1455,32 @@ func player_backpic_kind() -> String: ## `GetTrainerBackPic` has two: the player, and the old man who borrows the ## screen for the catching tutorial. if _data != null and _data.generation == RomRegistry.GEN1: - return Gen1Layout.PLAYER_BACKPICS[1 if _world_battle_tutorial else 0] + return _gen1_tutor().get("tutor", Gen1Layout.PLAYER_BACKPICS[0]) \ + if _world_battle_tutorial else Gen1Layout.PLAYER_BACKPICS[0] if _world_battle_tutorial: return "dude" return "kris" if player_is_female() else "chris" +func _gen1_tutor() -> Dictionary: + var values: Variant = _world_battle_request.get("values", _world_battle_request) + var raw: int = int((values as Dictionary).get("gen1_battle_type", 0)) \ + if values is Dictionary else 0 + return Gen1Layout.BATTLE_TYPES.get(raw, {}) + + +## Crystal's POKE_BALL is a TOWN MAP on the other cartridge. +func _tutor_ball() -> int: + return Gen1Layout.ITEM_POKE_BALL if _generation() == RomRegistry.GEN1 \ + else Gen2WorldPartyHost.ITEM_POKE_BALL + + +func _tutor_name() -> String: + if _generation() != RomRegistry.GEN1: + return DUDE_NAME + return String(Gen1Layout.TUTOR_NAMES.get(String(_gen1_tutor().get("tutor", "")), DUDE_NAME)) + + ## `wPlayerGender`'s `PLAYERGENDER_FEMALE_F`. Gold and Silver have one player ## character and no gender screen, so the answer there is always the male one. func player_is_female() -> bool: @@ -1527,6 +1568,10 @@ func _build_entrance() -> void: _entrance_stages.append({"apply": ENTRANCE_ENEMY_HUD}) # `DoBattle`'s own `ld c, 40`, then `SlideBattlePicOut` and `SendOutMonText`. _entrance_stages.append({"delay": PLAYER_ENTRANCE_FRAMES}) + ## `StartBattle` sends the player out on a normal battle alone; a Safari or + ## a tutor's fight opens `DisplayBattleMenu` with the back pic still up. + if _gen1_special_battle(): + return _entrance_stages.append({"slide": Gen2Battle.PLAYER}) _entrance_stages.append({ "message": SEND_OUT_LINES[ @@ -1913,7 +1958,7 @@ const GEN1_BREAK_FREE_TEXT: Array[String] = [ ## `Text_GotchaMonWasCaught` and `_ItemUseBallText05`. const CAUGHT_TEXT: String = "Gotcha! %s was caught!" -const GEN1_CAUGHT_TEXT: String = "All right! %s was\ncaught!" +const GEN1_CAUGHT_TEXT: String = "All right!\n%s was%scaught!" ## `SendOutMonText`'s four texts, in [constant Gen2Battle.SEND_OUT_GO]'s order. const SEND_OUT_LINES: Array[String] = [ @@ -2057,7 +2102,6 @@ func _begin_animation(event: Dictionary) -> void: ## The doll the animation would have drawn, written straight into the picture ## when the battle-scene option is off and the script never runs. -## ## `BattleCommand_LowerSub` and `..._RaiseSub` both branch to their own `noanim` ## routine on `_CheckBattleScene`, and `BattleCommand_Substitute`'s own ## `.no_anim` calls `RaiseSubNoAnim`, so the three animation parameters answer @@ -3243,10 +3287,13 @@ func complete_capture(result: Dictionary) -> Dictionary: var wobbles: int = clampi(int(result.get("wobbles", 0)), 0, 3) var caught: bool = bool(result.get("caught", false)) + ## `wCapturedMonSpecies`, which Yellow's initial catch training leaves standing + ## on a ball that broke out. + _capture_terminal = bool(result.get("ends_battle", false)) if caught: _box_queue.append( - (GEN1_CAUGHT_TEXT if _generation() == RomRegistry.GEN1 else CAUGHT_TEXT) - % _name_of(_enemy) + GEN1_CAUGHT_TEXT % [_name_of(_enemy), Gen2TextStream.SCROLL_BREAK] + if _generation() == RomRegistry.GEN1 else CAUGHT_TEXT % _name_of(_enemy) ) ## `.catch_bug_contest_mon` runs after `Text_GotchaMonWasCaught`, and ## `BugContest_SetCaughtContestMon`'s `.firstcatch` says a second line. @@ -3432,9 +3479,9 @@ func _play_poke_flute() -> void: ## animation behind it, and the next thing said is already the outcome. func _item_used_text(item: int) -> String: if _generation() == RomRegistry.GEN1: - ## `_ItemUseText001` and `_ItemUseText002` with the name between them: - ## one line, no "the", and an exclamation where Crystal has a stop. - return "%s used %s!" % [_player_label(), _item_name(item)] + ## `_ItemUseText001`, `text_low`, `_ItemUseText002`: no "the", the item on + ## the bottom row and an exclamation where Crystal has a stop. + return "%s used\n%s!" % [_player_label(), _item_name(item)] return "%s used the\n%s." % [_player_label(), _item_name(item)] @@ -3445,6 +3492,13 @@ func _is_wild_battle() -> bool: return values is Dictionary and StringName((values as Dictionary).get("kind", &"")) == &"wild" +## `wBattleType` on a Generation 1 cartridge: every value but zero is a fight +## the player's own Pokemon never joins. +func _gen1_special_battle() -> bool: + return _generation() == RomRegistry.GEN1 and _battle != null \ + and _battle.battle_type != Gen2Battle.BATTLETYPE_NORMAL + + ## `wBattleType` being BATTLETYPE_CONTEST, which is what makes the menu the ## contest's own and the ball a Park Ball. func _is_bug_contest_battle() -> bool: @@ -3479,7 +3533,6 @@ func _capture_failure(reason: StringName) -> Dictionary: ## `AskGiveNicknameText`, `YesNoBox` and `NamingScreen`, which `PokeBallEffect` ## runs for a caught Pokemon whether it went to the party or to the box, and ## which `.catch_bug_contest_mon` and `.FinishTutorial` both jump past. -## ## True while the prompt stands, so the pump that called this waits for it. func _open_capture_nickname() -> bool: if _capture_nickname_host != null: @@ -3757,7 +3810,6 @@ func _rules() -> Gen2Rules: ## Tries to run, which is `BattleMenu_Run` and settles before the turn does. -## ## Offered in a trainer battle too, because the cartridge offers it there and ## answers with its own refusal rather than greying the entry out. func run_from_battle() -> void: @@ -3770,7 +3822,6 @@ func run_from_battle() -> void: ## Swaps the player's Pokémon for the next one that is standing, as a turn. -## ## The enemy attacks while it happens, because a switch is not free: this is the ## whole point of the ordering rule, and it is worth being able to look at. func switch_player() -> void: @@ -4148,7 +4199,8 @@ func _finish_battle() -> void: func sync_live_party() -> bool: if _source_save == null or _battle == null or _data == null: return false - if _battle.in_battle_tower: + ## `.FinishTutorial` keeps nothing, and the tutor may stand on a stand-in party. + if _battle.in_battle_tower or _world_battle_tutorial: return true var fought: Gen2SaveData = Gen2SaveBattleAdapter.from_battle_party( _data.id, _data.sha1, _source_save.slot, _battle.party(Gen2Battle.PLAYER), @@ -4593,7 +4645,6 @@ func _take_turn_with_slot(slot: int) -> void: ## Opens `OfferSwitch`'s yes/no, which only SHIFT ever reaches, and answers ## whether there was a question to put up. -## ## Answered before a replacement for the same reason a Baton Pass is: the turn it ## stopped has not finished, and nothing behind it can be asked yet. func _answer_switch_offer() -> bool: @@ -4942,7 +4993,7 @@ func _enemy_label() -> String: ## sentence. func _player_label() -> String: if _world_battle_tutorial: - return DUDE_NAME + return _tutor_name() if _source_save != null and not _source_save.player_name.is_empty(): return _source_save.player_name return Gen2OakSpeech.DEFAULT_MALE @@ -5044,7 +5095,6 @@ func _draw_menu_layer() -> void: ## `DisplayCaughtContestMonStats`: the screen is cleared and the two boxes are ## drawn over it, STOCK #MON above THIS #MON, each with a name, a level and a ## HEALTH number, with `PlaceYesNoBox`' own box beside the lower one. -## ## One image on the menu layer, the way the party page is: all three boxes go ## into the tilemap on the cartridge too, and the text box under them is this ## screen's own, which is where `ContestAskSwitchText` is already being said. @@ -5289,7 +5339,6 @@ static func _count_row(item_name: String, quantity: int) -> Dictionary: ## `ItemSubmenu`'s USE/QUIT box, which stands over the list the row was chosen ## from and is what the second A press answers. -## ## On [member _info_layer] rather than on the menu layer, which is under the ## list: `LoadMenuHeader` draws this box into the tilemap after the pack's own, ## so it covers the rows it was opened from. @@ -5469,7 +5518,6 @@ func _replace_the_fallen() -> bool: ## The next event, with whatever it changes applied first. -## ## Every number drawn comes from the event, not the Pokémon: the turn has already ## resolved by the time the first event is shown, so reading the Pokémon would ## draw the end of the turn during the middle of it. @@ -6241,7 +6289,7 @@ func _button_pack(button: int) -> bool: ## and throws a POKE BALL anyway, so there is no USE submenu. if _world_battle_tutorial: _pack_selecting = false - _throw_ball(Gen2WorldPartyHost.ITEM_POKE_BALL, &"pack") + _throw_ball(_tutor_ball(), &"pack") else: _open_pack_action(&"pack") PokeButton.B: diff --git a/game/battle/damage.gd b/game/battle/damage.gd index 503a9fab..14b01065 100644 --- a/game/battle/damage.gd +++ b/game/battle/damage.gd @@ -123,7 +123,6 @@ static func damage_stats( ## `BattleCommand_DamageCalc`: Selfdestruct's halved defense, the formula, the ## type-boosting item, the critical multiplier, the cap and the minimum. -## ## Its `ret z` on a power of zero is why a status move reaches ## [method stab_damage] with no damage rather than the minimum two. ## [param level] is -1 for the attacker's own; `BattleCommand_BeatUp` is the one @@ -171,7 +170,6 @@ static func damage_calc( ## over whatever [method damage_calc] left. It runs for a powerless move too, and ## has to: `DoPoison` and `DoParalyze` carry `stab` and no `damagecalc`, which is ## how Thunder Wave learns it does nothing to a Ground type. -## ## Returns { damage, stab, immune, effectiveness }. [param foresight] is ## `SUBSTATUS_IDENTIFIED`, which drops the rows past the chart's `-2` marker. static func stab_damage( @@ -456,7 +454,6 @@ const CONSTANT_DAMAGE_EFFECTS: Array[int] = [ ## and by `AIDamageCalc`, which routes the same four effects here rather than ## through the formula. [param defender] is whoever the hit lands on, which is ## the only mon Super Fang reads. -## ## A null [param rng] is a prediction rather than a hit, and answers Psywave with ## the top of its range the way [constant MAX_VARIATION] does for the formula. static func constant_damage( @@ -489,7 +486,6 @@ static func psywave_damage(level: int, rng: RandomNumberGenerator) -> int: ## The split is by type, not by move: below Fire is physical and Fire up ## special, which is why Hyper Beam is physical and Bite special. -## ## A type past the cartridge's chart is a mod's own and carries the choice on its ## row instead, since there is no number to compare it against. Only such a ## number reaches the overlay, so a cartridge battle pays one comparison. @@ -516,7 +512,6 @@ static func _attack_stat( ## The defending stat, doubled by the defender's screen. Metal Powder is not ## here: `DittoMetalPowder` runs past `TruncateHL_BC`, which is ## [method metal_powder_pair]. -## ## `PlayerAttackDamage` doubles before `CheckDamageStatsCritical`, and a critical ## reaching `.thickclub`'s no-carry branch reloads the unmodified stat over it, ## so a critical that ignores the defender's stages ignores its screen too. diff --git a/game/battle/effect_commands.gd b/game/battle/effect_commands.gd index 621ed63a..6e2ce3c5 100644 --- a/game/battle/effect_commands.gd +++ b/game/battle/effect_commands.gd @@ -829,7 +829,6 @@ static func _rage_damage(turn: Gen2Turn) -> void: ## takes, and the line that says so. It stands behind `checkfaint`, which ends ## the move when the target has fallen, so a Rage that faints does not build; a ## substitute is not a gate, the doll spending the hit still counting. -## ## `inc a / ret z` is the saturation: 255 increments to 0 and is not stored. static func _build_opponent_rage(turn: Gen2Turn) -> void: if turn.missed: @@ -855,7 +854,6 @@ static func _check_future_sight(turn: Gen2Turn) -> void: ## Stores damage after DamageCalc and before DamageVariation, exactly where the ## source copies `wCurDamage` into the side's delayed word, and ends the move. -## ## `.failed` is a count still running: the move announces and then fails, which is ## what a second Future Sight does. static func _future_sight(turn: Gen2Turn) -> void: @@ -957,7 +955,6 @@ static func _stab(turn: Gen2Turn) -> void: ## `BattleCommand_DamageVariation`: the 85% to 100% spread, last. -## ## Nothing below two is touched and, because the routine returns before ## `BattleRandom`, nothing below two draws either, so a move that worked out to ## nothing moves no generator. @@ -1049,7 +1046,6 @@ static func _present(turn: Gen2Turn) -> void: ## `BattleCommand_FuryCutter`: the damage doubled once per consecutive hit, ## capped at five turns' worth, and the count reset by a miss. -## ## Sits between `stab` and `damagevariation`, so the doubling lands on the ## matched-up damage and the spread is taken from the doubled figure. static func _fury_cutter(turn: Gen2Turn) -> void: @@ -1152,7 +1148,6 @@ static func _tri_status_chance(turn: Gen2Turn) -> void: ## `BattleCommand_Defrost`: Flame Wheel and Sacred Fire thaw whoever used them. -## ## The user, not the target, which is what tells this apart from the `Defrost` ## subroutine [method _defrost] is. It clears the freeze bit rather than the ## whole status byte, which comes to the same thing while a freeze is the only @@ -1903,7 +1898,6 @@ static func _effect_chance(turn: Gen2Turn) -> void: ## Puts a status on the defender, or fails. One at a time: a Pokémon already ## carrying something is refused, as is one whose type makes it immune, and only ## sleep is rolled for a length. -## ## The four `*Target` commands share their order: existing status, weather, type, ## and only then `wEffectFailed`. Last is not cosmetic, the first step being the ## one that does something besides refuse: a burn whose roll failed still reaches @@ -2228,7 +2222,6 @@ static func _check_charge(turn: Gen2Turn) -> void: ## `BattleCommand_Charge`: the charging turn. It locks the user in, says its own ## line and ends the move; Skull Bash carries on at [constant END_TURN], which is ## where its Defense raise sits. -## ## A user that is asleep gets `PrintButItFailed` instead, which is Sleep Talk ## reaching a two-turn move: the source spends `movedelay` and `raisesub` before ## the line. @@ -3045,7 +3038,6 @@ static func _destiny_bond(turn: Gen2Turn) -> void: ## `BattleCommand_ForceSwitch`: Whirlwind and Roar, with two endings. Against a ## trainer the target's side switches to a random standing member; against a wild ## the battle *ends* in either direction, `SetBattleDraw` making both a draw. -## ## The source's `.trainer` and `.vs_trainer` are one routine here, every ## difference between them being which side is read: the user's level against the ## target's, the user having moved second, and a random member of that party. @@ -3226,7 +3218,6 @@ static func _foresight(turn: Gen2Turn) -> void: ## `BattleCommand_LockOn`: Lock On and Mind Reader, one command. The flag goes on ## the target, and [method _check_hit] spends it on the next hit check made ## against that Pokémon, whoever makes it. -## ## The one refusal is a target behind a doll, and it prints ## `PrintDidntAffect` rather than "But it failed!". static func _lock_on(turn: Gen2Turn) -> void: @@ -3321,7 +3312,6 @@ static func _thief(turn: Gen2Turn) -> void: ## `BattleCommand_Pursuit`: twice the finished figure against a side that is ## leaving, saturating at a word rather than wrapping. -## ## The doubling is all this command is. What makes Pursuit hit the Pokémon on its ## way out is `PursuitSwitch`, which runs the whole move in front of the switch; ## see [method Gen2Battle.take_actions]. @@ -3336,7 +3326,6 @@ static func _pursuit(turn: Gen2Turn) -> void: ## stats. `damagecalc` never sees `damagestats`, so no item, screen, stage or ## truncation touches either figure, and there is no `stab`, so the modifier ## stays `EFFECTIVE` and no effectiveness is announced. -## ## A member with no health or any status takes `.beatup_fail`, which skips ## forward to `buildopponentrage`: the hit is not spent and the loop carries on. static func _beat_up(turn: Gen2Turn) -> void: @@ -3400,7 +3389,6 @@ static func _base_stat(turn: Gen2Turn, species: int, key: String) -> int: ## `BattleCommand_CheckSafeguard`: the target's own Safeguard refusing a status ## move outright, with `SafeguardProtectText` and the move ended. -## ## `wAttackMissed` is set before the text, so everything behind this in the list ## is skipped and the move counts as a miss for whatever reads that back. static func _check_safeguard(turn: Gen2Turn) -> void: @@ -3420,7 +3408,6 @@ static func _safeguard_refuses(turn: Gen2Turn, side: int) -> bool: ## `BattleCommand_Heal`: Recover, Softboiled and Milk Drink take back half the ## maximum; Rest takes back all of it and pays for that with two turns asleep. -## ## The full-HP refusal is checked before anything else, so Rest at full health ## fails and stays awake even when there is a status sitting on it that sleeping ## would have cleared. Rest writes `REST_SLEEP_TURNS + 1` over the whole status @@ -3449,7 +3436,6 @@ static func _heal(turn: Gen2Turn) -> void: ## `BattleCommand_TimeBasedHealContinue`: Morning Sun, Synthesis and Moonlight. -## ## Half the maximum by default. One step down the table outside the move's own ## time of day, which is the cartridge's real rule: matching the clock buys ## nothing, missing it costs. Then one step up in sun, or one step down in any @@ -3496,7 +3482,6 @@ static func _heal_fraction(max_hp: int, index: int) -> int: ## Thunder's accuracy for this turn: `50 percent + 1` (128) in sun, and ## `100 percent` (255) in rain. -## ## Written on the turn rather than on the move, because the cartridge writes it ## into `wPlayerMoveStruct`, a per-turn copy, while [member Gen2Turn.move] is the ## cached row every future Thunder would read. @@ -3518,7 +3503,6 @@ static func _skip_sun_charge(turn: Gen2Turn) -> void: ## `PlayFXAnimID`. Nothing is drawn here: the animation is written down as an ## event at its own place in the turn and the screen is what spends frames on it. -## ## [param on_opponent] is `PlayOpponentBattleAnim`'s pair of ## `BattleCommand_SwitchTurn` calls: the same event with `hBattleTurn` inverted ## for the length of the animation, so it plays on the target rather than on @@ -3551,7 +3535,6 @@ static func _play_opponent_battle_anim(turn: Gen2Turn, index: int) -> void: ## `BattleCommand_MoveAnimNoSub`: the damage flash aimed at whoever was hit, the ## animation param cleared or alternated, and then the move's own animation. -## ## A miss falls to `BattleCommand_MoveDelay` and plays nothing. That branch is ## structural here rather than reached: [method _check_hit] ends the move, so no ## list gets this far after one. @@ -3580,7 +3563,6 @@ static func _move_anim(turn: Gen2Turn) -> void: ## `BattleCommand_LowerSub`: the user's doll dropped out of the way of whatever ## is about to be drawn, as the SUBSTITUTE animation's own `.dropsub` branch. -## ## Nothing is dropped for a user with no doll up, and nothing is dropped on the ## turn a two-turn move is charging either: `CheckUserIsCharging` is what ## [method _do_turn] reads as [member Gen2Turn.locked] or @@ -3646,7 +3628,6 @@ static func _stat_change_anim(turn: Gen2Turn, after_anim: int) -> void: ## Which of the five status commands carries an `AnimateCurrentMove` of its own. -## ## `BattleCommand_SleepTarget`, `..._Poison` and `..._Paralyze` are the status ## moves' own commands and do; `..._PoisonTarget`, `..._ParalyzeTarget`, ## `..._BurnTarget` and `..._FreezeTarget` are the secondary-effect commands and @@ -3664,7 +3645,6 @@ static func _status_move_animates(turn: Gen2Turn, flag: int) -> bool: ## Which status animation `PlayOpponentBattleAnim` plays on the target, or -1. -## ## The four secondary-effect commands each play one and the primary status moves' ## commands play none, each ending at `AnimateCurrentMove`; Toxic reaches ## `.apply_poison` too, so [method _toxic_target] plays none either. The exact @@ -3700,7 +3680,6 @@ static func _animate_current_move(turn: Gen2Turn) -> void: ## `BattleCommand_HeldFlinch`: a King's Rock on the attacker makes an ordinary ## attack flinch, out of the item's own parameter. -## ## The `wAttackMissed` guard is structural here, since [method _check_hit] ends ## the move on a miss and [method _check_faint] ends it on a KO, so this step is ## only ever reached by a hit that left the target standing. The Substitute check @@ -3859,7 +3838,6 @@ static func _stat_message(turn: Gen2Turn) -> void: ## only place [code]data/moves/effects.asm[/code] follows a message step with ## [code]statdownfailtext[/code]; an on-hit drop blocked by Mist fails silently, ## like any on-hit drop that misses its roll. -## ## Mist gets its own line, because ## [code]BattleCommand_StatDownFailText[/code] prints ## [code]ProtectedByMistText[/code] here rather than "won't go any lower". diff --git a/game/battle/move_effect.gd b/game/battle/move_effect.gd index 3b19c4ed..4926d76a 100644 --- a/game/battle/move_effect.gd +++ b/game/battle/move_effect.gd @@ -9,7 +9,6 @@ extends RefCounted ## The effect bytes with a list of their own, numbered as the cartridge's move ## table numbers them. -## ## Recoil is here because Struggle needs it: without it two empty Pokémon never ## finish their battle. The rest are the status conditions in their two shapes, a ## move whose whole purpose is the status and a move that damages and leaves @@ -1030,7 +1029,6 @@ const BATON_PASS_SEQUENCE: Array = [ ] ## Whirlwind and Roar. `checkhit` is the one thing in front of the command. -## ## The list carries no `failuretext`, so on the cartridge a missed Whirlwind ## reaches `forceswitch`, takes `.missed` and says "But it failed!" with no miss ## line at all. Here `checkhit` ends the move and announces the miss, which is @@ -1113,7 +1111,6 @@ const PURSUIT_SEQUENCE: Array = [ ## Beat Up: one pass of the loop per party member, `endloop` jumping back to ## `critical` rather than to the top, so `checkhit` is outside the loop and rolls ## once for the whole move. -## ## No `damagestats` and no `stab`: the command loads the formula's two stats ## itself, from base stats rather than from either Pokémon's real ones, and ## nothing multiplies the result by a matchup. `CheckTurn` leaves `wTypeModifier` @@ -1198,7 +1195,6 @@ const MEAN_LOOK_SEQUENCE: Array = [ ## [constant MULTI_HIT] and [constant DOUBLE_HIT]. `startloop` and `endloop` ## bracket the hit and `endloop` rewinds to `critical`, so the accuracy roll and ## the doll are outside the loop and the damage is worked out again per hit. -## ## The count is rolled by `endloop` on its first pass, which is *behind* the ## first hit's own spread: rolling it in front would take the same seed to a ## different battle. @@ -2116,7 +2112,6 @@ static func _table() -> Dictionary: ## The commands a move with this effect byte is made of. -## ## A registered effect wins over the cartridge's, which is what lets a mod ## rewrite one as well as add one. [method register_effect] is where that is ## refused for the effects the engine relies on reading back off a turn. @@ -2149,7 +2144,6 @@ const RESERVED_EFFECTS: Array[int] = [ ## Registers the command list a move carrying [param effect] runs. -## ## Every step named has to be one the engine knows or one already registered ## through [method register_command], so a list that would push an error mid-turn ## is refused here, where the mod's id is still in hand. @@ -2181,7 +2175,6 @@ static func register_effect(id: StringName, effect: int, commands: Array) -> Dic ## Registers a step a command list may name, run with the [Gen2Turn] the way ## every built-in step is. -## ## The engine's own commands are tried first, so a registration cannot shadow ## [constant Gen2EffectCommands.APPLY_DAMAGE] and quietly change what every move ## in the game does. diff --git a/game/battle/party.gd b/game/battle/party.gd index 53779a68..6be8e811 100644 --- a/game/battle/party.gd +++ b/game/battle/party.gd @@ -2,11 +2,9 @@ class_name Gen2Party extends RefCounted ## The Pokémon one side of a battle has, and which of them is out. -## ## An ordered list and a cursor into it, not a set: the order is what a switch ## menu shows and what a replacement is chosen from, and the first entry leads. A ## wild battle is a party of one, so it needs no special case. -## ## Fainted Pokémon stay in the list, still counting against the six. A battle is ## lost when all of them are down, not when the list runs out. @@ -88,7 +86,6 @@ func first_healthy() -> int: ## Puts [param index] out, and answers whether it went. -## ## The Pokémon leaving keeps its health and PP and loses its stat stages and ## everything [Gen2Substatus] holds. A stage is a lens on a stat, and neither it ## nor confusion, a charge or a recharge survives the walk back to the ball. diff --git a/game/data/world_catalog.gd b/game/data/world_catalog.gd index a51476da..1843e35b 100644 --- a/game/data/world_catalog.gd +++ b/game/data/world_catalog.gd @@ -6,7 +6,6 @@ extends RefCounted ## trades, prizes, ground items, badges and shops. The host owns the decoding and ## the mod owns the placement, so a randomizer needs no private copy of cartridge ## semantics. A patch changes a FIELD of a row and never the script behind it. -## ## Nothing is imported for this: every row is derived from the cache and written ## beside it as a sidecar, so an absent or stale one is rebuilt rather than bumped. @@ -131,7 +130,6 @@ static func build_reporting( ## The scan's result, for the sidecar. Ids are dictionary keys, so they go out ## as decimal strings and come back as ints; every one is well under the 2^53 ## a JSON number carries exactly. -## ## The lazy answers ([member _item_sources], [member _field_hms]) are not here: ## both are derived from these rows in a millisecond and would only be a second ## copy to keep in step. @@ -187,7 +185,6 @@ static func from_dict(data: GameData, source: Variant) -> Gen2WorldCatalog: ## and a `kind` comes back as floats and a String. A reader compares these with ## `==` against typed literals, so the shapes have to be restored rather than ## coerced at every site. -## ## Whole floats become ints: every number a row carries is an id, a bank, an ## address, an item, a quantity or a price, and none of them is fractional. The ## two String fields that are StringNames name themselves. @@ -278,7 +275,6 @@ func size() -> int: ## The site a command at [param bank]:[param address] belongs to but is not ## itself, as `{id, role}`, or empty. `role` is `picture` for a starter's ## `pokepic` and `price` for a prize's `checkcoins` or `takecoins`. -## ## This is what makes a patched field effective at the whole TRANSACTION rather ## than at one command of it: the ball that shows a Bellsprout hands over a ## Bellsprout, and a prize the mod priced at 500 is refused at 499 coins and @@ -806,7 +802,6 @@ func _store(row: Dictionary) -> void: ## it checked and the items it asked for. The decoded graph fact a placement ## needs, and no more than a fact: it does not say the site is unreachable ## without them, only that the cartridge looked. -## ## Read in source order up to the site rather than over the whole script, since a ## `checkevent` after a `givepoke` guards something else. func _requirements(commands: Array, at: int, crystal: bool) -> Array: diff --git a/game/gen1/gen1_importer.gd b/game/gen1/gen1_importer.gd index 1445d568..c2cd3d5a 100644 --- a/game/gen1/gen1_importer.gd +++ b/game/gen1/gen1_importer.gd @@ -1796,14 +1796,10 @@ func _import_pics( trainers, trainer_class - 1 ) - var player_back: Dictionary = PokeTiles.new_atlas( - Gen1Layout.BACKPIC_TILES, Gen1Layout.PLAYER_BACKPICS.size() - ) - for slot: int in Gen1Layout.PLAYER_BACKPICS.size(): - _decode_pic( - codec, rom, int(layout["pic_%s_back" % Gen1Layout.PLAYER_BACKPICS[slot]]), - player_back, slot - ) + var backpics: Array[String] = Gen1Layout.player_backpics(layout) + var player_back: Dictionary = PokeTiles.new_atlas(Gen1Layout.BACKPIC_TILES, backpics.size()) + for slot: int in backpics.size(): + _decode_pic(codec, rom, int(layout["pic_%s_back" % backpics[slot]]), player_back, slot) var player_front: Dictionary = PokeTiles.new_atlas( Gen1Layout.FRONTPIC_MAX_TILES, PLAYER_FRONTPICS.size() @@ -1815,7 +1811,7 @@ func _import_pics( var wanted: Dictionary = { "front": Gen1Layout.SPECIES_COUNT, "back": Gen1Layout.SPECIES_COUNT, "trainers": Gen1Layout.TRAINER_CLASS_COUNT, - "player_back": Gen1Layout.PLAYER_BACKPICS.size(), + "player_back": backpics.size(), "player_front": PLAYER_FRONTPICS.size(), } var atlases: Dictionary = { diff --git a/game/gen1/gen1_layout.gd b/game/gen1/gen1_layout.gd index 202c3116..7f80e02b 100644 --- a/game/gen1/gen1_layout.gd +++ b/game/gen1/gen1_layout.gd @@ -1194,7 +1194,8 @@ const SCRIPT_CALLS: Array[String] = [ "delay_frame", "delay_frames", "delay_3", "play_default_music", "check_map_trainers", "player_coords_in_array", "start_trainer_battle", "end_trainer_battle", "force_bike_or_surf", "play_music", "stop_all_music", "random", "set_sprite_position_2", "set_sprite_image", - "set_sprite_image_2", "enable_pikachu_drawing", "check_pikachu_following", + "set_sprite_image_2", "enable_pikachu_drawing", "disable_pikachu_drawing", + "check_pikachu_following", "set_sprite_facing", "set_sprite_facing_delay", "sprite_stay", "move_sprite", "decode_rle", "decode_arrow_movement", "update_gym_gates", "serial_connect", "fill_memory", "save_end_battle_text", "engage_map_trainer", @@ -1310,6 +1311,33 @@ const BATTLE_OUTCOME_SOURCES: Dictionary = { "is_in_battle": [0xFF, BATTLE_OUTCOME_LOST], "battle_result": [2, BATTLE_OUTCOME_ESCAPED], } +## `wBattleType` read across onto Crystal's numbering. The old man and Yellow's +## Prof. Oak share `DisplayBattleMenu`'s branch, so both are the Dude's tutorial +## here; `tutor` names `LoadPlayerBackPic`'s pic and `.oldManName`'s row. +const BATTLE_TYPE_OLD_MAN: int = 1 +const BATTLE_TYPE_SAFARI: int = 2 +const BATTLE_TYPE_PIKACHU: int = 4 +const BATTLE_TYPES: Dictionary = { + BATTLE_TYPE_OLD_MAN: {"battle_type": Gen2Battle.BATTLETYPE_TUTORIAL, "tutor": "old_man"}, + BATTLE_TYPE_SAFARI: {"battle_type": Gen2Battle.BATTLETYPE_SAFARI}, + BATTLE_TYPE_PIKACHU: {"battle_type": Gen2Battle.BATTLETYPE_TUTORIAL, "tutor": "prof_oak"}, +} +const TUTOR_NAMES: Dictionary = {"old_man": "OLD MAN", "prof_oak": "PROF.OAK"} +## `wCurOpponent` as each tutor's script writes it, by dex number. +const TUTOR_WILDS: Dictionary = { + RomRegistry.RED: {BATTLE_TYPE_OLD_MAN: 13}, RomRegistry.BLUE: {BATTLE_TYPE_OLD_MAN: 13}, + RomRegistry.YELLOW: {BATTLE_TYPE_OLD_MAN: 19, BATTLE_TYPE_PIKACHU: 25}, +} +const TUTOR_WILD_LEVEL: int = 5 +## `OldManItemList` is fifty balls where Yellow's `SimulatedInputBattleItemList` is one. +const TUTOR_BALLS: Dictionary = {RomRegistry.RED: 50, RomRegistry.BLUE: 50, RomRegistry.YELLOW: 1} +## `DisplayBattleMenu`'s two `DelayFrames` and `DisplayListMenuIDLoop`'s one. +const TUTOR_FRAMES: Dictionary = { + RomRegistry.RED: [80, 50, 80], RomRegistry.BLUE: [80, 50, 80], + RomRegistry.YELLOW: [20, 20, 20], +} +## Yellow's `ItemUseBall` answers `$63` while EVENT_INITIAL_CATCH_TRAINING stands. +const INITIAL_CATCH_TRAINING_EVENT: Dictionary = {RomRegistry.YELLOW: 47} ## Routines named by a full ROM offset, the same address in another bank being another routine. const SCRIPT_BANKED_CALLS: Array[String] = [ "coin_box", "music_rival_start", "music_rival_tempo", "schedule_pikachu_spawn", @@ -1337,7 +1365,7 @@ const SCRIPT_SILENT_CALLS: Array[String] = [ ## `SetSpritePosition2` puts back what `GetSpritePosition2` saved on the same ## visit, which the map's own table answers here; the image index is drawn. "set_sprite_position_2", "set_sprite_image", "set_sprite_image_2", - "enable_pikachu_drawing", + "enable_pikachu_drawing", "disable_pikachu_drawing", ## Red and Blue spell `StopAllMusic` as `PlaySound`; only Yellow has a routine. "play_music", "stop_all_music", ## A wait is frames of nothing and the map music is nobody's here. The three @@ -1415,6 +1443,8 @@ const SCRIPT_SILENT_STORES: Array[String] = [ "menu_watched_keys", "last_menu_item", "menu_item_to_swap", "print_item_prices", "list_menu_id", "filtered_bag_count", "walk_bike_surf_state_copy", "pikachu_spawn_state", + ## `AddPartyMon`'s catch-rate byte, read back by a Time Capsule alone. + "party_mon_1_catch_rate", ] ## The same over two bytes; `LoadItemList` already left the list itself. const SCRIPT_SILENT_WORDS: Array[String] = ["list_pointer"] @@ -1757,9 +1787,8 @@ const TRAINER_PIC_TILES: int = 7 const FRONTPIC_MAX_TILES: int = 7 const BACKPIC_TILES: int = 4 -## `GetTrainerBackpic`'s counterpart, in the `player_back` atlas's slot order: -## the player, and the old man who borrows the screen for the catching tutorial. -const PLAYER_BACKPICS: Array[String] = ["player", "old_man"] +## `LoadPlayerBackPic`'s three in atlas slot order; Red and Blue have no Prof. Oak. +const PLAYER_BACKPICS: Array[String] = ["player", "old_man", "prof_oak"] const RED_BLUE: Dictionary = { "species_names": 0x1C21E, @@ -2187,6 +2216,7 @@ const RED_BLUE: Dictionary = { "route23_copy_badge_text": 0x5125D, "add_party_mon": 0x3927, "mon_data_location": 0xCC49, + "party_mon_1_catch_rate": 0xD172, "beat_gym_flags": 0xD72A, "gym_leader_no": 0xD05C, "random_add": 0xFFD3, @@ -2460,6 +2490,7 @@ const YELLOW: Dictionary = { ## Yellow moved both back pics out of "Pics 4" and into their own bank. "pic_player_back": 0xF43B1, "pic_old_man_back": 0xF4441, + "pic_prof_oak_back": 0xF44D2, "pic_player_front": 0x11A97, "pic_shrink_1": 0x11B96, "pic_shrink_2": 0x11BF0, @@ -2625,6 +2656,7 @@ const YELLOW: Dictionary = { "route23_copy_badge_text": 0x51216, "add_party_mon": 0x391C, "mon_data_location": 0xCC49, + "party_mon_1_catch_rate": 0xD171, "beat_gym_flags": 0xD729, "gym_leader_no": 0xD05B, "random_add": 0xFFD3, @@ -2657,6 +2689,7 @@ const YELLOW: Dictionary = { "pikachu_spawn_state": 0xD430, "schedule_pikachu_spawn": 0xFC4FA, "enable_pikachu_drawing": 0x1525, + "disable_pikachu_drawing": 0x152D, "check_pikachu_following": 0x154A, "player_moving_direction": 0xD527, "play_music": 0x2211, @@ -2906,6 +2939,32 @@ static func flute_counts_wild(id: StringName) -> bool: return id == RomRegistry.YELLOW +static func player_backpics(layout: Dictionary) -> Array[String]: + var out: Array[String] = [] + for name: String in PLAYER_BACKPICS: + if layout.has("pic_%s_back" % name): + out.append(name) + return out + + +static func battle_type_values(raw: int) -> Dictionary: + var row: Dictionary = BATTLE_TYPES.get(raw, {}) + if row.is_empty(): + return {} + var out: Dictionary = {"battle_type": int(row["battle_type"]), "gen1_battle_type": raw} + if row.has("tutor"): + out["tutorial"] = true + out["can_lose"] = false + return out + + +## `.oldManBattle`'s throw. +static func tutorial_ball_lands(id: StringName, raw: int, event_active: Callable) -> bool: + if raw != BATTLE_TYPE_OLD_MAN or not INITIAL_CATCH_TRAINING_EVENT.has(id): + return true + return not bool(event_active.call(int(INITIAL_CATCH_TRAINING_EVENT[id]))) + + ## `CheckIfInOutsideMap`: which maps write `wLastMap` on the way out of them. static func is_outside_tileset(tileset: int) -> bool: return tileset == TILESET_OVERWORLD or tileset == TILESET_PLATEAU diff --git a/game/gen1/gen1_world_importer.gd b/game/gen1/gen1_world_importer.gd index 3522896c..88f6c09a 100644 --- a/game/gen1/gen1_world_importer.gd +++ b/game/gen1/gen1_world_importer.gd @@ -725,15 +725,18 @@ static func _read_map( var states: Dictionary = _read_map_states( rom, layout, bank, rom.u16le(header + MAP_SCRIPT_AT), texts ) + ## A hidden event's row can sit past every object's, as the Mansion's four + ## switches do, so the hidden rows are read before the table is grown. + var hidden: Array = _read_hidden_events( + rom, layout, map_id, bank, rom.u16le(header + MAP_SCRIPT_AT) + ) + events["hidden_events"] = hidden ## A row the table grew by may reach one higher still, which is how the ## Safari Zone gate's own six are read rather than its first four. while _extend_texts(rom, layout, bank, rom.u16le(header + 5), texts, events, states, known): states = _read_map_states( rom, layout, bank, rom.u16le(header + MAP_SCRIPT_AT), texts ) - var hidden: Array = _read_hidden_events( - rom, layout, map_id, bank, rom.u16le(header + MAP_SCRIPT_AT) - ) for row: Dictionary in hidden: _bind_map_script_byte(row.get("script", []) as Array, int(states["byte"])) @@ -1580,13 +1583,20 @@ static func _hidden_coin_nodes( if index < 0: return [] var flag: int = Gen1Layout.engine_flag_base("obtained_hidden_coins") + index - var found: String = predef_text(rom, layout, bank, "found_hidden_coins") - var dropped: String = predef_text(rom, layout, bank, "dropped_hidden_coins") + var amount: int = Gen1Layout.hidden_coin_amount(argument) + ## Both boxes print `hCoins`, which holds the amount just added. + var coins: int = int(layout["money_hram"]) + Gen1Layout.COIN_BUFFER_AT + var found: String = _number_filled( + predef_text(rom, layout, bank, "found_hidden_coins"), coins, amount + ) + var dropped: String = _number_filled( + predef_text(rom, layout, bank, "dropped_hidden_coins"), coins, amount + ) if found.is_empty() or dropped.is_empty(): return [] return [{"op": "has_item", "item": Gen1Layout.ITEM_COIN_CASE, "else": [], "then": [ {"op": "branch", "flag": flag, "engine": true, "then": [], "else": [ - {"op": "add_coins", "amount": Gen1Layout.hidden_coin_amount(argument)}, + {"op": "add_coins", "amount": amount}, {"op": "flag", "flag": flag, "set": true, "engine": true}, {"op": "has_coins", "coins": Gen1Layout.HIDDEN_COIN_CEILING, "test": "exactly", @@ -1918,11 +1928,26 @@ static func _script_ended(state: Dictionary, out: Array) -> Variant: return out -static func _script_printed(state: Dictionary, out: Array, box: Dictionary) -> void: +## A `text_bcd` over a buffer the walk wrote every byte of prints that number, +## which is how Yellow's Safari gate says ¥500 where Red's text spells it. +static func _script_printed(ctx: Dictionary, state: Dictionary, out: Array, box: Dictionary) -> void: + var layout: Dictionary = ctx["layout"] + for name: String in Gen1Layout.SCRIPT_BCD_BUFFERS: + var value: int = _script_bcd_value(state, name) + if value >= 0: + box["text"] = _number_filled(String(box["text"]), int(layout[name]), value) out.append(box) state["last_box"] = box +## `PrintBCDNumber` with LEADING_ZEROES | LEFT_ALIGN, which every map text's +## `text_bcd` passes: the digits alone. +static func _number_filled(text: String, address: int, value: int) -> String: + return Gen2TextStream.fill_all_markers( + text, "%s%04X>" % [Gen2TextStream.NUMBER_MARKER, address], str(value) + ) + + ## One instruction: the next address, [constant SCRIPT_END] or SCRIPT_UNREAD. ## The register writes are here and [method _script_flow] has the rest. static func _script_step( @@ -3603,6 +3628,16 @@ static func _script_special_body(ctx: Dictionary, pc: int, state: Dictionary = { return [{"op": "badge_guards", "rows": rows, "past_y": compares[0], "past_x": compares[1]}] +## `ResetButtonPressedAndMapScript` behind `EndTrainerBattle` zeroes +## `wCurMapScript`, so an end-battle state that calls it and prints on, as the +## Elite Four's do, ends on the default state rather than itself. +static func _script_silent_call(routine: String, state: Dictionary, out: Array, next: int) -> int: + if routine == "end_trainer_battle": + state["a"] = 0 + _script_map_script_mirror(state, out) + return next + + ## The routines a row may call. No node carries a sound, so a cry and the wait ## behind it spend nothing and the walk carries on past them. static func _script_call( @@ -3615,7 +3650,7 @@ static func _script_call( _script_untested(state) var routine: String = _script_routine(layout, target) if routine in Gen1Layout.SCRIPT_SILENT_CALLS: - return next + return _script_silent_call(routine, state, out, next) var shaped: int = _script_shaped_routine(ctx, target, state, out) if shaped == SCRIPT_NOT_SHAPED: shaped = _script_card_key_call(ctx, target, state, out) @@ -4849,7 +4884,8 @@ static func _script_bag_scan(ctx: Dictionary, at: int, out: Array) -> int: if item < 1 and op == Gen1Layout.SCRIPT_CP_N \ and rom.u8(at + offset + 1) != Gen1Layout.MAP_COORD_END: item = rom.u8(at + offset + 1) - taken = taken or (op == Gen1Layout.SCRIPT_JP \ + ## Yellow spells the tail `call` and `ret` where Red and Blue `jp`. + taken = taken or (op in [Gen1Layout.SCRIPT_JP, Gen1Layout.SCRIPT_CALL] \ and rom.u16le(at + offset + 1) == int(layout.get("remove_item_from_inventory", -1))) if item < 1 or not taken: return SCRIPT_SHAPE_REFUSED @@ -5307,7 +5343,7 @@ static func _script_text_row( out.append({"op": "facility", "command": int(decoded["command"])}) return true if not String(decoded["text"]).is_empty(): - _script_printed(state, out, {"op": "text", "text": String(decoded["text"])}) + _script_printed(ctx, state, out, {"op": "text", "text": String(decoded["text"])}) var code: int = _text_code_at(decoded) if code < 0: return true @@ -5580,15 +5616,25 @@ static func _read_trainer_header( + rom.u8(at + int(offsets["flag_bit"])), "sight_range": rom.u8(at + int(offsets["range"])) >> Gen1Layout.TRAINER_RANGE_SHIFT, } + ## The LIFT KEY's `ShowObject` is machine code in a header text, Red's + ## after-battle one and Yellow's end-battle one. for name: String in ["before", "after", "end"]: - out[name] = _read_trainer_text( - rom, layout, bank, rom.u16le(at + int(offsets[name])) - ) + var pointer: int = rom.u16le(at + int(offsets[name])) + out[name] = _read_trainer_text(rom, layout, bank, pointer) + var decoded: Dictionary = Gen1Text.decode_stream(rom, Gen1Layout.banked(bank, pointer)) + var code: int = _text_code_at(decoded) + if code < 0: + continue + var script: Array = decode_script(rom, layout, bank, code) + if script.is_empty(): + continue + ## Lance's `SetEvent EVENT_BEAT_LANCE` stands behind the streamed line. + if not String(decoded.get("text", "")).is_empty(): + script.push_front({"op": "text", "text": String(decoded["text"])}) + out["%s_script" % name] = script return out -## One of a header's three texts. `RocketHideoutB4FRocket3AfterBattleText` is -## the corpus's one that is machine code, and says its `PrintText` operand. static func _read_trainer_text( rom: RomFile, layout: Dictionary, bank: int, pointer: int ) -> String: diff --git a/game/import/battle_anim_importer.gd b/game/import/battle_anim_importer.gd index 8bc1a2db..7894a5d4 100644 --- a/game/import/battle_anim_importer.gd +++ b/game/import/battle_anim_importer.gd @@ -5,7 +5,6 @@ extends RefCounted ## in data/battle_anims the objects it spawns are built from. Each is a pointer ## table immediately followed by its data with every pointer bank-local, so each ## is cached as a whole region and a cached address resolves by subtraction. -## ## Validation walks what it stores, which is what makes a wrong offset a refused ## import rather than a screen full of noise. The object graphics are the ## exception: LZ streams reached by far pointer, decoded into tile strips. @@ -163,7 +162,6 @@ static func read_battle_anims(rom: RomFile, layout: Dictionary) -> Dictionary: ## `BattleAnimations` and every body it reaches, as one region. -## ## The walk follows calls and branches the way [Gen2BattleAnimScript] does, so a ## body only some other body jumps into is still measured, and refuses anything ## that leaves the bank or never reaches a top-level `anim_ret`. @@ -271,7 +269,6 @@ static func _walk_body(bank_bytes: PackedByteArray, address: int) -> Dictionary: ## `BattleAnimSineWave`, the table `BattleAnim_Sine` reads. -## ## The bytes are the check as well as the content: 64 identical bytes appear ## several times in a dump, so the offset is only trustworthy against the values, ## and this is the one copy in the animation bank. @@ -383,7 +380,6 @@ static func _read_framesets(rom: RomFile, table: int) -> Dictionary: ## One `oamframe` stream, to whichever of the three terminators ends it. -## ## An `oamframe` and an `oamwait` are both two bytes, because `.GetPointer` ## indexes frames by `frame * 2`. The three terminators are one byte each in the ## macros and the byte after them belongs to the next frameset, which the diff --git a/game/import/rom_cache.gd b/game/import/rom_cache.gd index 43b9322d..c807fc83 100644 --- a/game/import/rom_cache.gd +++ b/game/import/rom_cache.gd @@ -77,7 +77,7 @@ const BYTES_KEY: String = "bytes" ## a dump the owner still has, so re-importing costs a few seconds and a ## migration would have to carry every past shape forever. Nothing but the cache ## is thrown away, since saves live under their own root. -const FORMAT_VERSION: int = 128 +const FORMAT_VERSION: int = 132 ## What [method state] answers. A stale cache is told from a missing one because ## they need different things said to whoever is looking at it: one is a diff --git a/game/input/input_actions.gd b/game/input/input_actions.gd index 92be48b6..a41387ff 100644 --- a/game/input/input_actions.gd +++ b/game/input/input_actions.gd @@ -29,7 +29,6 @@ const DEADZONE: float = 0.5 const ALL_DEVICES: int = -1 ## The pad button Godot's own UI actions are missing. -## ## `ui_accept` ships as Enter, Keypad Enter and Space, and `ui_cancel` as Escape ## alone, so on a machine with no keyboard every focus ring in the launcher can ## be moved and nothing under it can be chosen. The page pair and the menu key @@ -200,7 +199,6 @@ static func defaults() -> Dictionary: ## Replaces every `gen2_*` action in the [InputMap] with these bindings. -## ## Rebuilt rather than merged: a rebind that only added would leave the previous ## key working, and the player would have two keys where the screen says one. static func install(scheme: Dictionary) -> void: @@ -387,7 +385,6 @@ static func action_badge( ## Reads a scheme back out of the options file. -## ## Clamped rather than refused, the way the rest of [Gen2Options] is: one ## unreadable binding should cost that binding, and a button left with none at ## all falls back to its default rather than becoming unpressable. diff --git a/game/launcher/launcher_ui.gd b/game/launcher/launcher_ui.gd index 7effba3d..3f762716 100644 --- a/game/launcher/launcher_ui.gd +++ b/game/launcher/launcher_ui.gd @@ -77,7 +77,6 @@ static func apply_display_density(window: Window, on: bool) -> void: ## Keeps [param root]'s window drawn in launcher units for as long as [param root] ## is in the tree, and puts it back when it leaves. -## ## Every screen written in launcher units needs this and only those do, so it is ## a guard a screen attaches rather than something a shell happens to own: the ## save editor is drawn in the same units and has no shell, and without the @@ -201,7 +200,6 @@ static func muted(theme: Gen2LauncherTheme, text: String) -> Label: ## A muted word that does not wrap, for a line of short facts. -## ## [method muted] wraps, and a wrapping label reports almost no minimum width, so ## one dropped into a flow row or an expanding box is squeezed to a character a ## line. A fact is short enough to keep whole and long enough to be worth the @@ -242,7 +240,6 @@ static func row(separation: int = GAP_MD) -> HBoxContainer: ## Empties a container that is about to be rebuilt. -## ## Every launcher pane is rebuilt from a button inside it, so the node emitting ## `pressed` is one of the children being removed and `free()` there destroys an ## object the signal is still walking. Detaching first takes the child out of the diff --git a/game/mods/mod_catalogue.gd b/game/mods/mod_catalogue.gd index d6f349f7..69bd93b4 100644 --- a/game/mods/mod_catalogue.gd +++ b/game/mods/mod_catalogue.gd @@ -16,7 +16,6 @@ const SOURCE_FILE_LABEL: String = "Installed from a file" ## The groups to draw, in the order to draw them: each followed source that has ## anything in it, in the order the player added them, then the file group. -## ## [param sources] is [method PokeModIndex.followed]'s rows, [param listings] is ## feed to that feed's entries, [param manifests] is ## [method Gen2ModHost.manifests] and [param present] is @@ -66,7 +65,6 @@ static func groups( ## One row of the list. [param entry] is the source's listing for it, empty for a ## mod no source lists; [param manifest] is the installed copy, null for one that ## is only listed. -## ## `version` is what the row shows: the installed version when there is one, ## because that is the copy the player has, and the listed version otherwise. ## [param present] is whether the mods root holds a directory for it, which is @@ -106,7 +104,6 @@ static func _row( ## What the row's own action does, which is the one place the Cydia rule lives: ## a listed mod is downloaded, updated or reinstalled, and an installed one is ## removed. Removing a listed mod leaves it listed. -## ## `replace` is a copy on disk this host would not load, which is what a ## contract bump leaves behind: the installer refuses a plain download over it, ## so the press has to say it is replacing what is there. diff --git a/game/mods/mod_host.gd b/game/mods/mod_host.gd index b1d68e60..40fa502a 100644 --- a/game/mods/mod_host.gd +++ b/game/mods/mod_host.gd @@ -5,7 +5,6 @@ extends RefCounted ## never touches scene nodes or engine internals: it is handed this host, ## registers what it provides, and is done, reaching only cartridge content ## through [GameData] and live world state through [Gen2WorldAPI]. -## ## Both renderers are replaceable this way, neither requiring 2D, which is why ## the contract is a factory and [method select_view] swaps between them. Mods are ## interpreted GDScript, iOS forbidding JIT and runtime native loading. @@ -26,7 +25,6 @@ const BATTLE_RENDERER_METHODS: Array[String] = [ ## rectangle at window resolution instead of the 160x144 viewport. A view built ## from geometry cannot be drawn into a 160x144 buffer and magnified, so this is ## what makes a 3D or HD renderer possible at all. -## ## A renderer that does not define it draws in hardware pixels, which is what the ## built-in ones do and what a tile-recolouring mod wants. Shared by both ## renderer kinds. @@ -71,7 +69,6 @@ const RENDERER_INPUT_METHOD: String = "handle_world_input" ## Optional, battle renderers only. The same seam on the battle side: every event ## [Gen2BattleScreen] did not claim, so a renderer composing its own shot can let ## someone steer it. Answering true consumes the event. -## ## A [PokeButton] never arrives here, on either side. The screen routes every one ## of them to whatever owns it first, so a text box, the forget-move list and ## ball selection all take their press before a renderer could see it, and what @@ -98,7 +95,6 @@ const RENDERER_INTERFACE_MASK_METHOD: String = "set_interface_masked" ## Optional, both renderer kinds, and only meaningful on the native layer. Called ## beside [constant RENDERER_RESIZE_METHOD] with the cartridge's own 160x144 ## screen expressed in that layer's pixels. -## ## Every hardware-pixel number a renderer is handed -- the text box's rectangle, ## first -- has to land somewhere inside the surface it draws on. Framed, that ## mapping was the surface itself, because it was a whole multiple of 160x144. @@ -167,7 +163,6 @@ const FIRST_MOD_POCKET: int = 5 ## steps along, a whole number in a range, and a button that does something the ## moment it is pressed. A button stores nothing, because "recentre the camera ## now" has no value to keep. -## ## A number is not a ladder with every rung written out: a randomizer's seed has ## ten thousand, and four one-digit ladders spend four menu rows on one field. const OPTION_LADDER: StringName = &"ladder" @@ -319,7 +314,6 @@ static func reset() -> void: ## Registers a world renderer under [param id]. -## ## [param script] is instantiated per world, so one registration serves a map ## change, a snapshot restore and a live switch between renderers. func register_world_renderer( @@ -463,7 +457,6 @@ func request_battle_message(id: StringName, text: String) -> Dictionary: ## The sounds a notice may ask for, by name, so a mod never names a raw effect ## number and never reaches one the host has not chosen to lend. -## ## `SFX_SHINE` ($5E) is deliberately absent: the sparkle means a shiny Pokemon ## and nothing else, and a mod firing it for something ordinary teaches a player ## to distrust it. `item` is `SFX_ITEM`, the jingle `FindItemInBallScript` plays, @@ -570,7 +563,6 @@ func set_hidden_items_source(source: Callable) -> void: ## The live world's own `{item: quantity}`, the copy a ## [method register_repel_renewal] provider is handed, and empty when no world is ## open. Read only, and a copy: writing the bag is the host's. -## ## One narrow accessor rather than a handle on [Gen2WorldAPI], because a ## non-renderer mod is deliberately given no world at all. func inventory() -> Dictionary: @@ -590,7 +582,6 @@ func set_progress_source(source: Callable) -> void: ## What the run being played has achieved: badges, the Hall of Fame, the dex ## counts, what is kept, money, coins, the play timer. `{}` with no world open. -## ## A copy rather than a handle, and read only: every field is state the host ## owns. See [Gen2ModProgress] for the fields and for why an absent one is absent ## rather than zero. @@ -611,7 +602,6 @@ func progress_for(save: Gen2SaveData, data: GameData = null) -> Dictionary: ## Re-reads the live run and emits [signal progress_changed] where a field moved. ## Called by [Gen2WorldScreen] once a world pass. -## ## Nothing is read while nothing is connected: walking the party and every box is ## the expensive half of a reading, and a build with no mod watching must not pay ## for it. @@ -1223,7 +1213,6 @@ func page(id: StringName) -> Dictionary: ## What [param id]'s page lists now, asked fresh: a page is a view of state the ## mod holds and is answered when it is drawn rather than when it is registered. -## ## Every row is normalised here, so a screen never sees a shape a mod invented: ## a row with no `label` is dropped, `icon` is a Dictionary or absent, and ## `locked` is a bool. An answer that is not an Array is no rows at all. @@ -1259,7 +1248,6 @@ func menu_entries(menu: StringName) -> Array: ## The start-menu entries [code]context[/code] leaves visible, in registration order. -## ## An entry that registered no `visible` predicate is always listed. One that did ## is asked with a copy of the context, so deciding whether to appear cannot ## change the menu being built; answering false leaves the row ABSENT rather than @@ -1726,7 +1714,6 @@ static func _option_name(id: StringName, key: StringName) -> String: ## Adds a species, move, item or trainer class the cartridge does not have. -## ## [param number] has to be at or above ## [constant Gen2ContentOverlay.FIRST_MOD_NUMBER], and [param row] is a partial ## row: whatever it leaves out comes from the kind's defaults. Everything a @@ -1780,7 +1767,6 @@ func patch_type_matchup( ## slots [method GameData.world_encounter] answers with, which is what a ## randomizer rewrites. [param method] is one of ## [constant Gen2ContentOverlay.ENCOUNTER_METHODS]. -## ## `slots` and `rates` are arrays and replace whole. Patching a map this ## cartridge does not carry changes nothing, exactly as a species patch does. func patch_encounter( @@ -1874,7 +1860,6 @@ func text_overlay() -> Gen2TextOverlay: ## Watches one of [constant CHANNELS]. [param handler] is called with each event ## dictionary as it reaches the screen showing it. -## ## Reading only. A subscriber is handed a copy of the event after the channel's ## optional presentation mutator has run. func subscribe(channel: StringName, id: StringName, handler: Callable) -> Dictionary: @@ -1928,7 +1913,6 @@ func unregister_event_mutator(channel: StringName, id: StringName) -> void: ## Hands [param event] through the optional presentation rewrite and then to ## every watcher. Returns the effective event for the screen to consume. -## ## Static and null-safe on the instance, because this sits on the path every ## battle event and every world result takes: a game with no mods must not build ## a host, or copy an event, to publish to nobody. @@ -2230,7 +2214,6 @@ func save_lifecycle_ids() -> Array: ## A save that has just been made, before it is written or played. A provider ## snapshots whatever its run is built from into its own namespace here; there is ## nothing to clear, since the save carries no run yet. -## ## The installation's mod settings are copied onto the save first, so the run ## records what it was created with and a later change to the installation cannot ## reach back into it. See [method PokeModOptions.bind_run]. @@ -2282,7 +2265,6 @@ func _owns_manifest(manifest: PokeModManifest) -> bool: ## Runs each discovered mod's entry script, which registers what it provides. -## ## A mod that will not load is reported and skipped: one broken mod must not ## stop the others, and it must not stop the game starting. A mod the player ## switched off is skipped silently and is not a failure, and a mod for another @@ -2440,7 +2422,6 @@ func loaded_mods() -> Array: ## Mounts a mod's own resource pack, once per run. -## ## `replace_files` is false, so a pack only adds paths and never lands on one the ## game ships. The engine has no unmount, which is why a reload remounts nothing ## and the set is kept on the host rather than the manifest. diff --git a/game/mods/mod_index.gd b/game/mods/mod_index.gd index 764bfff1..ecdfc312 100644 --- a/game/mods/mod_index.gd +++ b/game/mods/mod_index.gd @@ -49,7 +49,6 @@ const NOT_INSTALLED: StringName = &"not_installed" ## Turns whatever the player pasted into the feed URL to read. -## ## People copy whichever URL they happen to be looking at, so a repository page, ## a Pages site, a bare `owner/repo` and the feed file itself all resolve to the ## same place. Returns { ok, feed, label } or { ok: false, reason }. @@ -79,7 +78,6 @@ static func resolve_source(input: String) -> Dictionary: ## Reads a fetched feed into rows the launcher can list. -## ## Returns { ok, name, entries } where each entry has id, name, version, ## description, download, games, and the optional icon and thumbnail URLs. An ## entry missing an id or a usable download is dropped rather than failing the whole @@ -207,7 +205,6 @@ static func cached_feed(feed: String, directory: String = CACHE_DIRECTORY) -> Di ## What a fetch of [param feed] amounts to, whether or not it arrived. -## ## A feed that parsed is kept and answered; anything else falls back to the copy ## already on disk, so a server that is down costs the freshness of a listing ## rather than the listing. `stale` is true for the fallback, with `age` in @@ -331,7 +328,6 @@ static func _label_for(url: String) -> String: ## Whether [param feed] is the one this project publishes, which is followed by ## every build and cannot be dropped. -## ## Not `is_built_in`: [method Script.is_built_in] takes no arguments and a class ## name is a [Script], so that spelling resolves to Godot's and answers false ## everywhere without an error. diff --git a/game/options/options.gd b/game/options/options.gd index 485cc23a..b63cd4c4 100644 --- a/game/options/options.gd +++ b/game/options/options.gd @@ -2,11 +2,9 @@ class_name Gen2Options extends RefCounted ## Player options, in two independent blocks. -## ## The cartridge block is the real `wOptions` .. `wOptionsEnd` bytes, so the ## in-game OPTION menu and this launcher screen can never disagree about what a ## setting means. The app block is everything the hardware had no concept of. -## ## `data/default_options.asm` is byte identical between the two pins, so nothing ## here is profile split. @@ -167,7 +165,6 @@ func text_reveal_speed() -> float: ## Hardware frames per real second, as a multiple of the cartridge's own rate. -## ## Applied by [Gen2WorldAnimation.FrameClock] and nowhere else, which is what ## keeps it off the sound driver: [Gen2AudioPlayer] fills its generator from the ## output's own demand, so music, effects and cries run at the cartridge's tempo diff --git a/game/render/gen2_screen.gd b/game/render/gen2_screen.gd index 4ad186f4..785ffdb7 100644 --- a/game/render/gen2_screen.gd +++ b/game/render/gen2_screen.gd @@ -78,7 +78,6 @@ var subpixel: bool = false: var _subpixel_steps: int = 1 ## Whether the buffer outside the 160x144 rectangle is filled by this screen. -## ## On by default, and the reason a screen written without a thought for the ## window is still not letterboxed: a screen laid out in 160x144 has nothing of ## its own to put in a wider buffer, so the surround is its own field and the @@ -109,7 +108,6 @@ var surround_color: Color = Color.BLACK: _mask.queue_redraw() ## Whether anything has said what [member surround_color] is. -## ## "Not told yet" is not "told it is black", and the difference is a whole class ## of bug: a screen laid out over another -- a menu box over the map, a question ## over a picture -- has no field of its own, and painting the default over the @@ -204,7 +202,6 @@ func apply_screen_fill() -> void: ## Inside the screen, in hardware pixels: position it in the 160x144 space. -## ## Everything placed this way is interface, and sits above whatever ## [method display_content] put there, in the order it was placed. func display(node: Node) -> void: @@ -243,7 +240,6 @@ func view_size() -> Vector2i: ## The cartridge's own 160x144 screen, in the native layer's own pixels. -## ## A view on that layer is handed a rectangle and told nothing else, and every ## hardware-pixel number it is given -- the text box's, first -- has to land ## somewhere inside it. Framed, that mapping was the layer itself, since the @@ -350,7 +346,6 @@ func interface_layer() -> Control: ## Where the hardware's own 160x144 sits inside the buffer, in buffer pixels. -## ## What a screen drawing a backdrop has to leave alone: the picture it hands ## [method set_backdrop] is the buffer's size, and this is the hole its own ## frame is already filling. @@ -359,10 +354,8 @@ func interface_origin() -> Vector2i: ## The screen [param node] is drawn on, or null outside one. -## ## The walk rather than a stored reference: a screen is added to whichever host ## has one and freed by it, and neither end should have to hold the other. -## ## The outermost one, not the nearest, for a node that is somehow inside two. static func owner_of(node: Node) -> Gen2Screen: var at: Node = node @@ -431,7 +424,6 @@ func _on_backdrop_source_gone() -> void: ## The 160x144 field a screen stands on when it is a colour rather than a ## picture: a [ColorRect] that says what colour it is, so the screen around it ## carries the same one out to the window. -## ## [method Gen2PicImage.show] is that seam for a screen drawn as a picture and ## this is the other half of it. It reports on every draw rather than on a ## setter, so a fade that recolours the field takes the surround with it without @@ -658,7 +650,6 @@ func _draw_cover() -> void: ## the interface is laid out in. Not a filled surface, because the middle is not ## always the interface's -- a battle transition is the renderer's own screen and ## has to stay visible inside it. -## ## A backdrop the size of the buffer is drawn through the same four bands, so a ## screen with real art out there paints it and a screen without paints its own ## field colour. diff --git a/game/render/gender_screen_page.gd b/game/render/gender_screen_page.gd index 35fda354..bc143c65 100644 --- a/game/render/gender_screen_page.gd +++ b/game/render/gender_screen_page.gd @@ -3,11 +3,9 @@ extends RefCounted ## `engine/menus/init_gender.asm`'s screen on the hardware tile grid: the ## question in the standard text box and the two-option menu over it. -## ## Positions are the source's own. The menu is [Gen2MenuPage]'s, drawn from ## `.MenuHeader`'s `menu_coords 6, 4, 12, 9` and its three flags, so nothing ## about the box is decided here. -## ## Crystal only, since pokegold ships neither the routine nor its text. const TILE: int = Gen2Font.TILE @@ -70,7 +68,6 @@ static func menu_box() -> Gen2MenuBox: ## The whole 160x144 page as palette indices, with the arrow on [param cursor]. -## ## `InitGenderScreen` ByteFills the whole tilemap with tile $00, which ## `LoadGenderScreenLightBlueTile` has just filled with one colour, so the field ## behind the box and the menu is that index rather than a blank. diff --git a/game/render/pic_image.gd b/game/render/pic_image.gd index 1d3c6b1d..0b3a0dc7 100644 --- a/game/render/pic_image.gd +++ b/game/render/pic_image.gd @@ -5,7 +5,6 @@ extends RefCounted ## cartridge stores, two bits per pixel and no colour, so the palette is chosen ## here at draw time and a shiny sprite costs one [PackedColorArray] rather than ## a second copy of the pixels. -## ## Built as one buffer for [method Image.create_from_data]: per-pixel ## [method Image.set_pixel] on a 56x56 sprite is 3136 binding calls for a table ## lookup, and a battle can want several sprites a frame. Node-free, so headless. @@ -68,7 +67,6 @@ static func canvas_from_indices( ## tile, so a screen the hardware draws in several palettes is one buffer here ## too rather than a layer per colour. [param slots] is [param columns] wide and ## row-major, and a slot naming no palette falls back on the first. -## ## `FillBoxCGB` and `ByteFill` over an attrmap `WipeAttrmap` cleared are what ## every `_CGB_*` layout writes, so a caller's own `attributes()` is that list of ## boxes flattened once. @@ -167,7 +165,6 @@ static func from_atlas( ## The same cell before a palette is chosen: { indices, width, height }, or empty ## for a slot the atlas does not hold. Split out so a palette fade over a ## frontpic swaps colours rather than cropping the atlas again. -## ## A [param pic] carrying its own [code]indices[/code] is a mod's picture and has ## no cell to crop: the atlases hold exactly the cartridge's slots. Answering it ## here is what lets every screen draw one without knowing which it has. @@ -213,7 +210,6 @@ static func atlas_cell( ## One packed RGBA8 word per colour index, so a blit is an array copy rather ## than a [Color] conversion. Little-endian, which is the order ## [method PackedInt32Array.to_byte_array] writes and `FORMAT_RGBA8` reads. -## ## Public because every per-tile blit in the project shares it: a page that ## builds a [Color] per pixel pays a binding call and an allocation for a table ## lookup, which is what [method blit_tile] exists to stop. @@ -370,7 +366,6 @@ static func x_flipped_indices(indices: PackedByteArray, width: int) -> PackedByt ## The palette as the eight-bit colours a blit actually writes. -## ## For a caller comparing a rendered pixel against a palette: a 15-bit colour is ## kept as a float and read back through an eight-bit image, so the comparison ## belongs where the picture is and the conversion has to be the drawing's own. @@ -389,7 +384,6 @@ static func quantized( ## A blank canvas [param width] x [param height], one packed RGBA8 word per ## pixel. Every word is zero, which is transparent black. -## ## A word rather than four bytes because the inner loop of every page in the ## project is one store per pixel either way, and four of them cost four bounds ## checks; the conversion to bytes at the end is one memcpy. @@ -458,7 +452,6 @@ static func blit_tile( ## Puts [param image] on [param target]. -## ## The one way a screen in this project hands a redrawn picture to the node that ## shows it, so the reuse below happens everywhere rather than at whichever ## screen was measured. diff --git a/game/render/pokedex_page.gd b/game/render/pokedex_page.gd index da26ad39..da1151d6 100644 --- a/game/render/pokedex_page.gd +++ b/game/render/pokedex_page.gd @@ -265,7 +265,6 @@ static func blank_map() -> PackedInt32Array: ## `Pokedex_DrawMainScreenBG`: the left sidebar, the two boxes and the bottom ## bar. The listing is not in it; that is the window, and [method window_map] ## draws it. -## ## [param seen] and [param caught] are the two counts `CountSetBits` prints. func main_background(seen: int, caught: int) -> PackedInt32Array: _unown_letters = false @@ -317,7 +316,6 @@ func search_results_background(count: int, type_line: String) -> PackedInt32Arra ## `DrawPokedexListWindow` and `Pokedex_PrintListing`, as the twelve-column ## window the main screen and the results screen both put their listing in. -## ## [param rows] is [method Gen2Pokedex.rows]' own shape: `number`, `name`, ## `seen` and `caught` per visible entry. [param old_mode] swaps the scroll bar ## for the two tiles that replace it and is what prints a dex number above each @@ -402,7 +400,6 @@ func results_window_map(rows: Array) -> PackedInt32Array: ## `Pokedex_DrawDexEntryScreenBG` and `DisplayDexEntry`, as one grid. -## ## [param entry] is [method GameData.dex_entry]'s own record. A species that has ## not been caught keeps the placeholder height and weight the background draws, ## which is what the source's early `ret z` leaves on screen. @@ -529,7 +526,6 @@ func search_map( ## `Pokedex_DrawUnownModeBG` and its own letter walk. -## ## [param forms] is `wUnownDex`, the forms in catching order, and ## [constant UNOWN_COORDS] the table it indexes: the letter's cell and the cell ## the cursor stands in beside it, which are not always the same column. diff --git a/game/render/second_screen.gd b/game/render/second_screen.gd index 230a2613..4cabea3e 100644 --- a/game/render/second_screen.gd +++ b/game/render/second_screen.gd @@ -33,7 +33,6 @@ const FIELD_COLOR := Color(0.0, 0.0, 0.0, 1.0) ## What the panel shows with no world on it, drawn in the launcher's own ## language rather than the cartridge's: an empty cartridge silhouette, the ## project's name and a line saying nothing is running. -## ## The launcher measures in points, so the design is written in these units and ## drawn at whatever whole multiple of them the panel is; a launcher unit is ## about a point at [constant IDLE_UNITS] on a handheld's lower display. @@ -129,7 +128,6 @@ func set_world(data: GameData, world: Gen2WorldAPI, save: Gen2SaveData) -> void: ## Re-reads the world: which tabs are open, and whether the page on screen is ## still the one the cursor names. -## ## Called every hardware frame and does nothing on almost all of them: the gates ## are three numbers and the row's one live picture is the party's lead, so the ## whole question is a short string. Rebuilding the START menu here instead would @@ -168,7 +166,6 @@ func _gate() -> String: ## Which tab a tap at [param at] landed on, counting from zero, or -1 for a tap ## that missed the row. -## ## Static and pure, so where a touch lands is asserted without a display: the row ## is the only part of this screen that takes one. static func tab_index_at(at: Vector2, canvas: Vector2i, count: int) -> int: @@ -197,7 +194,6 @@ static func tab_cell(index: int, canvas: Vector2i, count: int) -> Rect2i: ## The tab the tap at [param at] landed on, in canvas pixels, or the empty name ## for a tap that missed the row. -## ## Public because a host on a real panel converts its own touch to these pixels ## and a test drives it without one. func tab_at(at: Vector2) -> StringName: @@ -208,7 +204,6 @@ func tab_at(at: Vector2) -> StringName: ## A touch on the tab row, in canvas pixels. Answers whether it opened a page. -## ## The only input this screen accepts, and the reason it stays read only: there ## is no path from here into a page. func touch(at: Vector2) -> bool: @@ -439,7 +434,6 @@ func _cell(index: int) -> Rect2: ## The row as the cartridge would have drawn it: the player's own text-box frame ## around white paper, with the open tab underlined in the frame's ink. -## ## Redrawn when the tab set or the chosen tab changes, not per frame. Nothing ## here is invented: the six frame tiles are `LoadFrame`'s own, chosen by the ## same FRAME option the boxes on the top screen wear. @@ -468,7 +462,6 @@ func _redraw_strip() -> void: ## The six frame tiles around the row. -## ## Placed by hand rather than through [method Gen2Font.draw_box], because that ## one takes whole tiles in both directions and this row is a whole number of ## tiles in neither: the border tiles are laid at the four edges and the runs diff --git a/game/render/second_screen_host.gd b/game/render/second_screen_host.gd index 9465144c..27e433c6 100644 --- a/game/render/second_screen_host.gd +++ b/game/render/second_screen_host.gd @@ -56,7 +56,6 @@ var _settle: int = PANEL_SETTLE_FRAMES ## Attaches [param view] to whatever second display this build can reach, or ## answers null when there is none. -## ## [param mode] is [member Gen2Options.second_screen]. The view is not built ## here: the world owns it, because it mirrors the world, and this only decides ## where it is drawn. @@ -192,7 +191,6 @@ func _attach_window() -> bool: ## The largest hardware-pixel canvas that fills [param panel] at a whole scale. -## ## Whole numbers only, for the reason [Gen2Screen] gives: a hardware pixel drawn ## as 6.43 screen pixels crawls. The leftover is a bar the far side fills with ## the field colour, and it is never more than one scale factor wide. diff --git a/game/render/town_map_page.gd b/game/render/town_map_page.gd index 4f8b716e..129894b5 100644 --- a/game/render/town_map_page.gd +++ b/game/render/town_map_page.gd @@ -254,7 +254,6 @@ func _load_sheet( ## The whole screen as tile numbers, in the order the source writes them: the ## region map over everything, then the screen's own frame, then the name. -## ## [param cards] is `wPokegearFlags`, as the card names `Pokegear_FinishTilemap` ## tests; it is read only by the Pokegear card's frame. [param name_codes] is the ## landmark's name, or the dex area's whole `'S NEST` header. @@ -586,7 +585,6 @@ func attributes(data: GameData, map: PackedInt32Array) -> PackedInt32Array: ## The whole screen as pixels, each tile coloured through the palette ## `TownMapPals` gave it. [param female] is Kris's own city colours. -## ## This cannot go through [method Gen2PicImage.from_indices] whole: the screen is ## six palettes at once. func image( diff --git a/game/rom/rom_registry.gd b/game/rom/rom_registry.gd index 97d9581f..dbc030c3 100644 --- a/game/rom/rom_registry.gd +++ b/game/rom/rom_registry.gd @@ -27,28 +27,29 @@ const CRYSTAL: StringName = &"crystal" ## sha1 (lowercase hex) -> { id, title, revision, generation, playable } ## `playable` is false while a generation has an importer but no story to walk: -## the launcher seats such a cartridge and reads it, and offers no Play. +## the launcher seats such a cartridge and reads it, and offers no Play. Every +## cartridge here walks from the bedroom to its Hall of Fame. const BY_SHA1: Dictionary = { "ea9bcae617fdf159b045185467ae58b2e4a48b9a": { "id": RED, "title": "Red", "revision": "USA/Europe", "generation": GEN1, - "playable": false, + "playable": true, }, "d7037c83e1ae5b39bde3c30787637ba1d4c48ce2": { "id": BLUE, "title": "Blue", "revision": "USA/Europe", "generation": GEN1, - "playable": false, + "playable": true, }, "cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1": { "id": YELLOW, "title": "Yellow", "revision": "USA/Europe", "generation": GEN1, - "playable": false, + "playable": true, }, "d8b8a3600a465308c9953dfa04f0081c05bdcb94": { "id": GOLD, diff --git a/game/rom/rom_verifier.gd b/game/rom/rom_verifier.gd index ac54064c..766f3f56 100644 --- a/game/rom/rom_verifier.gd +++ b/game/rom/rom_verifier.gd @@ -2,7 +2,6 @@ class_name RomVerifier extends RefCounted ## Identifies a user-supplied ROM file by SHA-1 against [RomRegistry]. -## ## Pure and node-free so the whole import gate is testable headlessly. Nothing ## here reads game content; it only answers "is this a cartridge we know?". ## The importer that follows is entitled to assume a verified hash. @@ -41,7 +40,6 @@ static func sha1_of_file(path: String) -> String: ## Identifies a candidate ROM. -## ## Returns { status, sha1, id, title, revision, message }. Callers should look ## at [code]status[/code]; [code]message[/code] is a ready-to-show string. static func identify(path: String) -> Dictionary: diff --git a/game/save/save_screen.gd b/game/save/save_screen.gd index c8f07d61..41bc4466 100644 --- a/game/save/save_screen.gd +++ b/game/save/save_screen.gd @@ -2,11 +2,9 @@ class_name Gen2SaveScreen extends Control ## Player-facing save selection for one imported cartridge revision. -## ## The screen only coordinates validated save data. Original SRAM bytes enter ## through [Gen2SramAdapter], and project slots are written through ## [Gen2SaveStore], so no control here needs to know a cartridge offset. -## ## It is the launcher's second screen and shares its frame: same shell, same ## soft surfaces, same palette. diff --git a/game/save/sram_adapter.gd b/game/save/sram_adapter.gd index 266b2c58..fa139eb3 100644 --- a/game/save/sram_adapter.gd +++ b/game/save/sram_adapter.gd @@ -3,7 +3,6 @@ extends RefCounted ## Boundary between an original Generation 2 SRAM image and the project's ## party-focused save model. -## ## Only an existing, checksummed cartridge image is written. The canonical save ## model does not own map, options, inventory, PC boxes or event flags, so ## creating those bytes from scratch would invent game state. Bytes outside the @@ -216,7 +215,6 @@ static func export_bytes( ## Refuses a save holding content a cartridge byte cannot name. -## ## Every species, item and move on the hardware is one byte, and ## [constant Gen2ContentOverlay.FIRST_MOD_NUMBER] sits past that on purpose. So a ## mod's own content has no representation here: truncating it would write a diff --git a/game/world/battle_tower.gd b/game/world/battle_tower.gd index 2b54d82c..dcb83400 100644 --- a/game/world/battle_tower.gd +++ b/game/world/battle_tower.gd @@ -4,7 +4,6 @@ extends RefCounted ## `SECTION "SRAM Battle Tower"` and the routines that read it: the challenge's ## own state, the seven trainers it has already sampled, and the levels, rules ## and rewards `engine/events/battle_tower/` decides from them. -## ## Scene free. The world screen owns the two menus and the battle; this record ## owns the SRAM section and every answer `BattleTowerAction`, `LoadOpponentTrainerAndPokemon` ## and `_CheckForBattleTowerRules` give from it. @@ -188,7 +187,6 @@ static func group_level(group: int) -> int: ## `BattleTower_LevelCheck`, which fails on the first party member above the ## chosen group's level. -1 when the party is inside it. -## ## [param party] is the read-only party mirror the script runner carries: ## `species`, `levels`, `held_items` and `eggs`, one entry per slot. static func level_check(party: Dictionary, group: int) -> int: @@ -219,7 +217,6 @@ static func ubers_check(party: Dictionary, group: int) -> int: ## `_CheckForBattleTowerRules`' four checks in its own order, each named by the ## box it prints. An empty Array is a party that may enter. -## ## `BattleTower_ExecuteJumptable` runs every check rather than stopping at the ## first, so a party can fail more than one and the receptionist says so once ## per failure. @@ -288,7 +285,6 @@ func load_opponent(data: GameData, random: RandomNumberGenerator) -> Dictionary: ## The `.resample` loop: `maskbits BATTLETOWER_NUM_UNIQUE_TRAINERS` is seven ## bits, so the roll is 0 to 127 and anything past the table is drawn again, as ## is any index already in `sBTTrainers`. -## ## Crystal 1.0 masks with `BATTLETOWER_NUM_UNIQUE_MON` instead and can only ever ## reach the first 21; the pinned dumps are 1.1, so this is the 1.1 branch. func _sample_trainer(random: RandomNumberGenerator) -> int: @@ -436,7 +432,6 @@ func choose_reward(random: RandomNumberGenerator) -> int: ## `BattleTower_GiveReward`: the stored item, or POTION when the pack has no room ## for five more of it. [param pack] is the item pocket as ## [code]{ item: quantity }[/code]. -## ## The source's own shape, which is not "is there room": a pack under `MAX_ITEMS` ## always has room for a new row, and a full one only does when it already ## carries the reward with fewer than `MAX_ITEM_STACK - 5 + 1` of it. @@ -452,7 +447,6 @@ func reward_for(pack: Dictionary) -> int: ## `BattleTowerAction`'s jumptable, as the value it leaves in `wScriptVar`. ## An action that writes nothing there answers -1, which the runner leaves the ## variable alone for. -## ## The mobile rows are no-ops with a reason: each writes a byte in SRAM bank 5 ## that only the mobile adapter's own routines read, and none of them reaches a ## script the player can talk to. diff --git a/game/world/game_freak_presents.gd b/game/world/game_freak_presents.gd index ad1de7df..7de3b74f 100644 --- a/game/world/game_freak_presents.gd +++ b/game/world/game_freak_presents.gd @@ -199,7 +199,6 @@ func word_positions() -> Array[Vector2i]: ## What the last `PlaySpriteAnimations` pass left in shadow OAM, as ## [code]{ kind, at, set, flip_y }[/code] in shadow-OAM coordinates. -## ## The buffer, not the live structs: `GameFreakPresentsInit` spawns one without ## writing shadow OAM, and a struct the scene half of a pass spawns is only drawn ## by the sprite half, which on Gold and Silver runs first and so does not see it @@ -211,7 +210,6 @@ func sprites() -> Array[Dictionary]: ## Which `GameFreakDittoPaletteFade` colour the Ditto is wearing, or -1 before ## `GameFreakLogo_Transform` has written one. -## ## Applied in the pass that computes it, like the shadow OAM beside it: ## `hCGBPalUpdate` and hDMATransfer are both serviced by the same VBlank, so the ## palette and the sprites reach the screen together and neither is delayed here. diff --git a/game/world/intro_movie.gd b/game/world/intro_movie.gd index 6d288b62..b9a9fe6e 100644 --- a/game/world/intro_movie.gd +++ b/game/world/intro_movie.gd @@ -11,7 +11,6 @@ extends RefCounted ## Crystal only: `GoldSilverIntro` is a different movie. ## `wBGPals2` and the `wOBPals2` behind it, which is what the screen shows. -## ## Every scene's `ld bc, 16 palettes` runs off the end of the background ## buffer and into the object one, which is where the sprites get their colours: ## the Unown's purple, Pichu's yellow and Wooper's blue are all in the second @@ -630,7 +629,6 @@ func _scene_reset_counter() -> void: ## The half of a setup scene every one of them shares: clear the palettes, the ## sprites and the tilemap, load this scene's own VRAM and palette run, put the ## scroll back at the origin and hand on. -## ## `Intro_ClearBGPals` spends two `DelayFrame`s of its own, which is why a setup ## scene is not free. func _setup_scene() -> void: @@ -1194,7 +1192,6 @@ func _run_sprites() -> void: ## `SpriteAnimFunc_IntroSuicune`: still until `wIntroSceneTimer` is set, then a ## jump on a sine of amplitude 32 and the second frameset under it. -## ## The reinit is unconditional in the source and has to stay that way: ## `_ReinitSpriteAnimFrame` puts FRAME back to -1 and DURATION to 0 every frame, ## so the jump holds `.Frameset_IntroSuicune2`'s first entry for the whole leap diff --git a/game/world/link_session.gd b/game/world/link_session.gd index 67c5a1ca..92d70335 100644 --- a/game/world/link_session.gd +++ b/game/world/link_session.gd @@ -102,7 +102,6 @@ func wait_for_linked_friend(transport: Gen2LinkTransport) -> int: ## `Link_CheckCommunicationError`: FALSE is a link that dropped while the two ## players were saving, and the peer's own action lands in ## `wOtherPlayerLinkMode` for the `readmem` that follows. -## ## A Gen 1 game has no `wPlayerLinkAction` to raise, so what comes back from one ## is zero, which is the whole of the "can't link to the past" branch and the ## whole of what makes the Time Capsule legal. @@ -188,7 +187,6 @@ func which_chris(transport: Gen2LinkTransport) -> int: ## tests: every species slot first, then mail, then moves. The answer is the ## wScriptVar value and the party slot the box names, which is the slot the test ## stopped on rather than the first slot of the party. -## ## [param party] is the world's own party mirror: the parallel `species`, ## `held_items` and `moves` arrays [method Gen2WorldAPI.set_party_summary] ## carries, which is where every other party-reading special gets its answer. @@ -252,7 +250,6 @@ static func validate_ot_trademon( ## `CheckAnyOtherAliveMonsForTrade`. A trade that would leave this side with ## nothing that can fight is refused, so the offered slot is skipped here and ## the incoming Pokemon is what answers for it. -## ## Carry set is the refusal in the source, so this answers the opposite: TRUE ## means the trade may go ahead. static func any_other_alive_mons_for_trade( @@ -271,7 +268,6 @@ static func any_other_alive_mons_for_trade( ## takes the last of the five, which `.FindOpponentAndAppendRecord`'s sort has ## kept for the least successful one. Both counters stop at ## [constant MAX_LINK_RECORD] rather than wrapping. -## ## [param result] is `wins`, `losses` or `draws`, which is `wBattleResult`'s own ## WIN/LOSE/DRAW one name further on. static func add_battle_to_record( diff --git a/game/world/move_deleter.gd b/game/world/move_deleter.gd index 37311690..76e80f39 100644 --- a/game/world/move_deleter.gd +++ b/game/world/move_deleter.gd @@ -2,7 +2,6 @@ class_name Gen2MoveDeleter extends RefCounted ## `MoveDeletion` (`engine/events/move_deleter.asm`), the rules half. -## ## Same shape as [Gen2NameRater], one house further on: an introduction and a ## `YesNoBox`, the party list `SelectMonFromParty` opens, two endings that need ## no move, `ChooseMoveToDelete`'s own list, a second `YesNoBox` and @@ -21,7 +20,6 @@ const SFX_MOVE_DELETED: int = 0x97 ## Which ending [param mon] reaches once it has been chosen, or `&""` for the ## one member the routine carries on with. -## ## `.onlyonemove` reads `wPartyMon1Moves + 1`, which is the *second* slot rather ## than a move count, so a member whose second slot is empty is refused whatever ## stands behind it. diff --git a/game/world/mystery_gift.gd b/game/world/mystery_gift.gd index a09898e6..059924cf 100644 --- a/game/world/mystery_gift.gd +++ b/game/world/mystery_gift.gd @@ -65,7 +65,6 @@ const LOCKED: int = 0xFF ## The section a slot that has never seen a Mystery Gift carries. -## ## `sMysteryGiftData` is two pairs rather than one block, and the pairing is ## what `BackupMysteryGift` and `RestoreMysteryGift` copy between: `item` and ## `unlocked` are the working bytes the exchange writes, `backup_item` and @@ -213,7 +212,6 @@ static func copy_decorations_to_pc( ## `MysteryGiftGetItem` and `MysteryGiftGetDecoration`, which share ## `MysteryGiftFallbackItem`: an index past either table's end is a RED CARPET ## as a decoration and a GREAT BALL as an item, the same byte read two ways. -## ## [param table] is the imported `MysteryGiftItems` or `MysteryGiftDecos`. static func gift_at(table: Array, index: int) -> int: if index < 0 or index >= table.size(): @@ -349,7 +347,6 @@ static func _add_partner_id(section: Dictionary, id: int) -> void: ## `.SaveMysteryGiftTrainerName`, which is what the Trainer House reads: the ## partner's name and the flag that says somebody has linked. -## ## The party beside them is deliberately empty, because a real cartridge's is: ## `StagePartyDataForMysteryGift` sits behind a `vc_patch` and is unreferenced ## in the retail build, so `ClearMysteryGiftTrainer` zeroes `wMysteryGiftTrainer` @@ -369,7 +366,6 @@ static func _item_name(data: GameData, item: int) -> String: ## `DoMysteryGiftIfDayHasPassed`, which is the only thing that resets the day's ## partner list and runs when the menu row is chosen rather than at midnight. -## ## `sMysteryGiftTimer` is `InitOneDayCountdown`'s own two bytes: the days still ## to run, and the day the countdown started on. `CheckDayDependentEventHL` ## takes the days since that start off the first byte and reports the borrow, diff --git a/game/world/pokedex.gd b/game/world/pokedex.gd index 2df7083e..3cb290db 100644 --- a/game/world/pokedex.gd +++ b/game/world/pokedex.gd @@ -5,7 +5,6 @@ extends RefCounted ## `wPokedexDataStart`..`wPokedexDataEnd` does: the mode, the species order that ## mode built, how far the listing runs, and the cursor and scroll into it. A ## screen draws [method rows] and feeds buttons back in. -## ## The orderings come from the cache ([method GameData.dex_order_new] and ## [method GameData.dex_order_alpha]); DEXMODE_OLD has no table, `.OldMode` ## counting from 1. Seen and caught come from [Gen2WorldState]. @@ -402,7 +401,6 @@ func _move_up_one_page() -> bool: ## `Pokedex_ListingMoveDownOnePage`, which always reports a change. -## ## The source adds two pages to the offset in one byte and treats the carry as ## "near the bottom", so an offset that would overflow lands on the last page ## exactly as one that runs past `wDexListingEnd` does. The wrap is kept because @@ -437,7 +435,6 @@ func toggle_page() -> void: ## `Pokedex_NextOrPreviousDexEntry`: moves until it lands on a seen species, and ## puts the cursor and scroll back if it runs out of listing first. -## ## Answers whether it moved. A move re-enters the entry screen at page 1, which ## is `Pokedex_ReinitDexEntryScreen`. func step_entry(button: int) -> bool: @@ -460,7 +457,6 @@ func step_entry(button: int) -> bool: ## The selected species' entry, as `DisplayDexEntry` prints it: ## { species, name, number, category, caught, height, weight, page, text }. -## ## The name, the category and the number are printed whether or not the species ## has been caught; the caught check sits after them and gates the measurements ## and the description. A zero height or weight is left blank rather than printed @@ -542,7 +538,6 @@ static func mode_rows(with_unown: bool = false) -> Array: ## `.ChangeMode`: choosing the mode already in use changes nothing, and choosing ## another reorders the listing and puts the cursor back at the top before ## seeking `wPrevDexEntry` again. -## ## Answers whether the mode changed, which is what decides whether the screen ## shows [constant CHANGING_MODES_TEXT]. func change_mode(next_mode: int) -> bool: @@ -587,7 +582,6 @@ func search_type_string(value: int) -> String: ## `Pokedex_UpdateSearchMonType`, which reads left and right on the two type rows ## only and answers whether the value changed. -## ## The two rows wrap differently, and deliberately: the first row runs 1 to ## NUM_TYPES and can never be empty, while the second wraps through ## [constant SEARCH_TYPE_NONE] as well, which is the only way to search on one diff --git a/game/world/pokedex_screen.gd b/game/world/pokedex_screen.gd index 0aa86828..02efac10 100644 --- a/game/world/pokedex_screen.gd +++ b/game/world/pokedex_screen.gd @@ -232,7 +232,6 @@ func _handle_list(button: int) -> bool: ## `Pokedex_UpdateDexEntryScreen`: B returns to the listing, A turns the page, ## and up and down step to the neighbouring entry. -## ## The source's four-button row is PAGE, AREA, CRY and PRNT; only PAGE is built, ## so A always turns the page rather than moving a cursor along a row whose ## other three entries would refuse. @@ -377,7 +376,6 @@ func _handle_option(button: int) -> bool: ## `.ChangeMode`, including the message it shows while the order is rebuilt. ## Choosing the mode already in use returns to the listing untouched. -## ## UNOWN is not one of them: `.MenuAction_UnownMode` never writes `wCurDexMode`, ## it jumps straight to DEXSTATE_UNOWN_MODE, so the listing keeps the mode it ## had and the Unown screen answers back to OPTION rather than to the listing. @@ -640,7 +638,6 @@ func _handle_search_results(button: int) -> bool: ## `Pokedex_InitSearchScreen`, which resets both type rows every time. -## ## Coming back from the results screen resets them too: `.return_to_search_screen` ## jumps to DEXSTATE_SEARCH_SCR, and that jumptable entry is this Init rather ## than its Update, so the search is not remembered. @@ -806,7 +803,6 @@ func _selected_pic() -> Image: ## One imported pic, or the question mark when the cache does not hold it. -## ## `_CGB_Pokedex` fills the picture box's attrmap with palette 1, and which ## palette that is turns on `wCurPartySpecies`: the two listing screens set it to ## `-1` and get `PokedexQuestionMarkPalette`, so every species is drawn in the diff --git a/game/world/pokegear_screen.gd b/game/world/pokegear_screen.gd index 63699930..21baf1dd 100644 --- a/game/world/pokegear_screen.gd +++ b/game/world/pokegear_screen.gd @@ -30,7 +30,6 @@ const CARD_MAP: StringName = &"map" ## engine/pokegear/pokegear.asm's card order. Each is behind its own ## wPokegearFlags bit, named by the engine flag that carries it, since that is ## what the state holds. The clock card needs no flag. -## ## Here rather than in the screen that lists them, because the Pokegear is not ## the only thing that has to know which cards a player owns. const CARDS: Array[Dictionary] = [ @@ -146,7 +145,6 @@ func _ready() -> void: ## names `Pokegear_FinishTilemap` tests, [param text] whichever of the Pokegear's ## own texts the card prints: the question it opens with and, on the phone card, ## `PokegearAskDeleteText`. -## ## Optional the way the region map is: a cache with no card tilemaps answers ## false and the caller keeps its own screen open. func open( diff --git a/game/world/prof_oaks_pc.gd b/game/world/prof_oaks_pc.gd index fecc6f8e..de082c55 100644 --- a/game/world/prof_oaks_pc.gd +++ b/game/world/prof_oaks_pc.gd @@ -3,7 +3,6 @@ extends RefCounted ## `ProfOaksPCBoot`, `ProfOaksPCRating` and `Rate` (engine/events/prof_oaks_pc.asm), ## as the pages a screen shows and the effect it plays. -## ## Presentation only: the routine counts `wPokedexSeen` and `wPokedexCaught` and ## writes nothing back. The pages are the source's own order, and each waits for ## A or B, which is why the sound is on the last of them rather than on the whole @@ -19,7 +18,6 @@ const COUNT_DIGITS: int = 3 ## The whole boot, as [code]{ seen, caught, sfx, pages }[/code]. Empty for a ## cache imported without the rating table, which is the caller's cue to leave ## the script's own `end` alone. -## ## `ProfOaksPCBoot` is `_OakPCText2` in front of `Rate`. static func boot(data: GameData, state: Gen2WorldState) -> Dictionary: var out: Dictionary = rate(data, state) diff --git a/game/world/town_map_screen.gd b/game/world/town_map_screen.gd index a70031cd..e8a4f7e2 100644 --- a/game/world/town_map_screen.gd +++ b/game/world/town_map_screen.gd @@ -114,7 +114,6 @@ func _ready() -> void: ## region map: `_TownMap`'s own corner box or the Pokegear card's icon row. ## [param cards] is the owned `wPokegearFlags` cards, which only the card frame ## reads. -## ## Optional the way the other overlays are: a cache with no region map answers ## false and the caller keeps its menu open. func open( @@ -207,7 +206,6 @@ func open_dex_area( ## visited. [param in_kanto] is which map `FlyMap` opens, which is the region the ## player is standing in, and [param visited] which `FLY_*` indexes ## `CheckIfVisitedFlypoint` answers for. -## ## The answer is taken with [method chosen_spawn] once this closes: -1 for a ## cancel, and the flypoint's own spawn for a choice, which is exactly the byte ## `.pressedA` leaves in `e`. @@ -259,7 +257,6 @@ func cursor_name() -> String: ## `.loop`'s own joypad read: B leaves and the d-pad walks the window. Every ## other button is swallowed, which is what the loop does with them. -## ## The dex area's loop leaves on A as well as B, walks regions rather than ## landmarks, and reads SELECT as a held state; see [method release_button]. func handle_button(button: int) -> bool: @@ -309,7 +306,6 @@ func close() -> void: ## One hardware frame. Only the player icon moves: the cursor's own ## `SPRITEANIMSTRUCT_ANIM_SEQ_ID` is overwritten with `SPRITE_ANIM_FUNC_NULL`, so ## it holds `.Frameset_StillCursor`'s single entry forever. -## ## The dex area animates nothing. Its own frame is `.BlinkNestIcons`, which ## shows the icons for sixteen frames and hides them for sixteen; the player ## icon it draws instead is `GetPlayerIcon`'s standing frame, not a walk. diff --git a/game/world/trainer_card.gd b/game/world/trainer_card.gd index fcb83cb8..51f29078 100644 --- a/game/world/trainer_card.gd +++ b/game/world/trainer_card.gd @@ -3,7 +3,6 @@ extends RefCounted ## What the trainer card prints (`engine/menus/trainer_card.asm`), as pages a ## screen can draw. -## ## Everything here comes from the save and the world the card is opened over: ## `wPlayerName`, `wPlayerID` and `wMoney` on the top half, `wPokedexCaught`'s ## set bits and `wGameTimeHours`/`wGameTimeMinutes` on page 1, and the two badge @@ -85,7 +84,6 @@ static func badges(state: Gen2WorldState, page_number: int, crystal: bool) -> Ar ## `TrainerCard_Page1_Joypad`, `..._Page2_Joypad` and `..._Page3_Joypad` as one ## table: which page a direction reaches, and whether A leaves. -## ## Page 1 answers right and A with page 2; page 2 answers left with page 1 and A ## with the exit; page 3 answers left with page 2 and right with page 1. The two ## `.KantoBadgeCheck` branches that would reach page 3 are unreferenced in both diff --git a/game/world/world_animation.gd b/game/world/world_animation.gd index b5adc943..5d88dcfe 100644 --- a/game/world/world_animation.gd +++ b/game/world/world_animation.gd @@ -2,7 +2,6 @@ class_name Gen2WorldAnimation extends RefCounted ## Small interpreter for the command tables used by the cartridge overworld. -## ## The original engine writes 2bpp tiles to VRAM. This node-free equivalent ## keeps the same command order and timer semantics, but applies those writes to ## the indexed tile strip held by GameData. @@ -22,7 +21,6 @@ const TIMER_TOWER_FRAMES: Array = [0, 1, 2, 3, 4, 3, 2, 1] const TIMER_WATER_PALETTE: Array = [0, 1, 2, 1] ## Real seconds into hardware frames, for every screen that spends them. -## ## One instance per pump, and the whole conversion: the remainder is banked here ## so nothing downstream keeps one, the cap is applied here so no screen can ## forget it, and [method Gen2Options.speed_scale] is applied here and nowhere else. @@ -252,7 +250,6 @@ func reload_tileset(world: Gen2WorldAPI, time_of_day: int = Gen2WorldPalette.TIM ## Runs one hardware frame's command and reports whether the tile strip or a ## palette row actually changed. -## ## Most commands are waits and timer bumps that draw nothing new, so the return ## value is what a renderer should gate its atlas rebuild on; [method tick] is ## the single-command step the sequence is defined in. diff --git a/game/world/world_api.gd b/game/world/world_api.gd index 92904af6..3fc7fbbd 100644 --- a/game/world/world_api.gd +++ b/game/world/world_api.gd @@ -2,7 +2,6 @@ class_name Gen2WorldAPI extends RefCounted ## Scene-free runtime access to one imported Generation 2 map. -## ## The API works in walk cells: every cell is a 2x2 group of 8x8 graphics ## tiles. It owns player position, camera framing and live object state, while ## event flags are supplied through a separate scene-free world state. @@ -558,7 +557,6 @@ func clear_map_name_sign() -> void: ## `InitMapNameSign`, which every map setup script but the submenu's reaches. -## ## The sign is Crystal's own screen: pokegold ships neither `MapEntryFrameGFX` ## nor the routine, so the whole decision is skipped there. `wPrevLandmark` is ## written on both branches, which is what makes a walk through a gate silent on @@ -1138,7 +1136,6 @@ func _clear_player_step() -> void: ## Spends one hardware frame of the player's walk-step offset. -## ## This only shrinks a presentation offset that starts and ends at player_cell; ## it never changes player_cell, collision or event results, and a caller that ## never starts a step sees no difference. @@ -3531,7 +3528,6 @@ func tile_index_at(tile_x: int, tile_y: int) -> int: ## Returns the visible 20x18 graphics-tile page in row-major order. Map padding ## is expanded through [method drawn_block_at], just like LoadMetatiles. -## ## This is [method tile_index_at] for 360 tiles, written out rather than called ## 360 times: it is on the draw path, and the per-tile call did the same block ## division and bounds check for every tile of the same block row. @@ -3613,7 +3609,6 @@ static func in_hardware_buffer(map: Gen2WorldMap, block_x: int, block_y: int) -> ## Every map the connection graph reaches from the current one, keyed ## `"group:number"`, each with its origin in the current map's block ## coordinates. The current map is not in it: it is the origin. -## ## Built once per map load and kept, since the graph is header data and nothing ## a run does moves a map. Ordered nearest first, so a caller that draws them in ## order draws the far ones under the near ones. @@ -3684,7 +3679,6 @@ static func connection_origin_blocks( ## The same fold for a map that is not the loaded one, which is what a battle ## staged on a map has: [Gen2BattleWorldContext] names the map and hands over no ## world, deliberately, so there is no `current_map` to read. -## ## [param block_overrides] is a live `changeblock` table, keyed as ## [method _block_key] keys one. A caller with no world has none, and a map with ## no world has had no block edited. @@ -3843,6 +3837,8 @@ const GEN1_BATTLE_OUTCOMES: Dictionary = { Gen1Layout.BATTLE_OUTCOME_ESCAPED: [ Gen2WorldBattleAdapter.OUTCOME_CAUGHT, Gen2WorldBattleAdapter.OUTCOME_RAN, ], + ## `wBattleResult` at zero, which the tower's MAROWAK reads with `and a`. + Gen1Layout.BATTLE_OUTCOME_WON: [Gen2WorldBattleAdapter.OUTCOME_WON], } ## What the last battle a script asked for answered. @@ -3991,7 +3987,6 @@ const GEN1_SCRIPT_NODES: Dictionary = { ## The raw cartridge permission byte at a walk cell. -## ## The imported grid already holds the code the tileset gave each cell, so it is ## the answer unless a changeblock has replaced the block this cell belongs to. ## An overridden block has to be looked up in the tileset instead, because the @@ -4108,7 +4103,10 @@ func _gen1_card_key_steps() -> Array: return [_gen1_card_key_box("card_key_fail")] door["type"] = &"block" door["card_key"] = true - return [_gen1_card_key_box("card_key_success"), door] + ## `set BIT_CUR_MAP_LOADED_1` behind the block: the floor's callback flags + ## the door on the next frame, so leaving and returning keeps it open. + return [_gen1_card_key_box("card_key_success"), door, + {"type": &"map_load", "bit": Gen1Layout.MAP_LOADED_1_BIT}] func _gen1_card_key_box(name: String) -> Dictionary: @@ -4393,12 +4391,12 @@ func _gen1_node_object_facing( ## The store `CallFunctionInTable` dispatches on next frame; a map with no ## dispatch has no byte for `wCurMapScript` to be copied into. -func _gen1_node_set_map_script(node: Dictionary, steps: Array, _run: Dictionary) -> bool: +func _gen1_node_set_map_script(node: Dictionary, steps: Array, run: Dictionary) -> bool: var byte: int = int(node["byte"]) if byte < 0: return true ## `wNextSafariZoneGateScript`: the state the walk before it is to land on. - var value: int = _gen1_saved_coord_index if node.has("from") else int(node["value"]) + var value: int = _gen1_run_saved_index(run) if node.has("from") else int(node["value"]) steps.append({"type": &"map_script", "byte": byte, "value": value}) return true @@ -4424,17 +4422,23 @@ func _gen1_node_battle_outcome(node: Dictionary, steps: Array, run: Dictionary) func _gen1_node_wild_battle(node: Dictionary, steps: Array, _run: Dictionary) -> bool: - steps.append({"type": &"request", "values": { - "kind": &"battle_requested", - "values": { - "kind": &"wild", - "pokemon": int(node["species"]), - "level": int(node["level"]), - }, - }}) + var values: Dictionary = { + "kind": &"wild", "pokemon": int(node["species"]), "level": int(node["level"]), + } + values.merge(Gen1Layout.battle_type_values(int(node.get("battle_type", 0)))) + steps.append({"type": &"request", "values": {"kind": &"battle_requested", "values": values}}) return true +func gen1_tutorial_ball_lands() -> bool: + if data == null or state == null: + return true + var values: Dictionary = pending_runtime_request().get("values", {}) + return Gen1Layout.tutorial_ball_lands( + data.id, int(values.get("gen1_battle_type", 0)), state.is_event_flag_active + ) + + ## `DecodeArrowMovementRLE`: the arrow tile the player stands on queues its own ## legs, and a cell with no row of its own leaves the map's trainers alone. func _gen1_node_arrow_movement(node: Dictionary, steps: Array, run: Dictionary) -> bool: @@ -4523,7 +4527,7 @@ func _gen1_node_saved_coord_index( node: Dictionary, steps: Array, run: Dictionary ) -> bool: return _gen1_resolve_side( - node, _gen1_index_matches(node, _gen1_saved_coord_index), steps, run + node, _gen1_index_matches(node, _gen1_run_saved_index(run)), steps, run ) @@ -4532,15 +4536,22 @@ func _gen1_index_matches(node: Dictionary, index: int) -> bool: return index < value if String(node["test"]) == "below" else index == value -## The store, whose -1 is `wCoordIndex` itself rather than a constant. +## The store, whose -1 is `wCoordIndex` itself. The run keeps the copy the rest +## of the row reads and the world takes it when spent: both sides of the gate's +## "Leaving early?" resolve before the answer, and NO's 5 had overwritten YES's 0. func _gen1_node_save_coord_index( - node: Dictionary, _steps: Array, run: Dictionary + node: Dictionary, steps: Array, run: Dictionary ) -> bool: var value: int = int(node["value"]) - _gen1_saved_coord_index = int(run.get("coord_index", 0)) if value < 0 else value + run["saved_coord_index"] = int(run.get("coord_index", 0)) if value < 0 else value + steps.append({"type": &"saved_coord_index", "value": int(run["saved_coord_index"])}) return true +func _gen1_run_saved_index(run: Dictionary) -> int: + return int(run.get("saved_coord_index", _gen1_saved_coord_index)) + + func _gen1_node_player_facing(node: Dictionary, steps: Array, _run: Dictionary) -> bool: steps.append({ "type": &"player_facing", @@ -5089,7 +5100,11 @@ func _gen1_node_heal_party(_node: Dictionary, steps: Array, _run: Dictionary) -> return true -func _gen1_node_hall_of_fame(_node: Dictionary, steps: Array, _run: Dictionary) -> bool: +## `HallOfFamePC` writes the team into `sHallOfFame` and counts it in +## `wNumHoFTeams` in front of the animation, which is the induction the shelf +## and the Pokemon Center's machine read as `ENGINE_HALL_OF_FAME`. +func _gen1_node_hall_of_fame(_node: Dictionary, steps: Array, run: Dictionary) -> bool: + _gen1_node_flag({"flag": Gen2WorldState.ENGINE_HALL_OF_FAME, "engine": true, "set": true}, steps, run) steps.append({"type": &"request", "values": { "kind": &"hall_of_fame_requested", "values": {}, }}) @@ -5146,21 +5161,23 @@ func _gen1_node_map_text(node: Dictionary, steps: Array, run: Dictionary) -> boo var text_id: int = int(node["text"]) if node.has("text") \ else _gen1_scratch_read(run, int(node["from"]), int(node["offset"])) ## `hTextID` and `hSpriteIndex` are one HRAM byte, so a row a map script - ## opens by id leaves that id where `EngageMapTrainer` reads a sprite. + ## opens by id leaves that id where `EngageMapTrainer` reads a sprite: the + ## object of that index is the trainer, which is how Lance's room fights him. _gen1_last_sprite_index = text_id - 1 var row: Dictionary = gen1_text_at(text_id) ## `DisplayTextID` over a trainer's row is `TalkToTrainer` after the fight. - var trainer: Array = _gen1_trainer_steps(row, {}) + var trainer: Array = _gen1_trainer_steps(row, _gen1_object_event(text_id - 1)) if not trainer.is_empty(): steps.append_array(trainer) return true var nodes: Variant = row.get("script", []) if nodes is Array and not (nodes as Array).is_empty(): return _gen1_resolve_script(nodes as Array, steps, run) - var text: String = gen1_filled_text(String(row.get("text", ""))) - if text.is_empty(): + if String(row.get("text", "")).is_empty(): return false - steps.append({"type": &"text", "text": text}) + ## `wStringBuffer` outlives the row that filled it: Lt. Surge's receipt is + ## the `DisplayTextID` after his `GiveItem`. + steps.append(_gen1_script_box(row, String(run.get("named", "")), run.get("buffers", {}))) return true @@ -5903,9 +5920,12 @@ func _gen1_trainer_steps(row: Dictionary, event: Dictionary) -> Array: var flag: int = int(header["event_flag"]) _gen1_scratch[int(Gen1Layout.for_id(data.id)["trainer_header_flag_bit"])] = flag % 8 if event_flag_active(flag): - return [{"type": &"text", "text": String(header["after"])}] + if header.has("after_script"): + var steps: Array = [] + return steps if _gen1_resolve_script(header["after_script"], steps, _gen1_run(event)) else [] + return [{"type": &"text", "text": gen1_filled_text(String(header["after"]))}] return [ - {"type": &"text", "text": String(header["before"])}, + {"type": &"text", "text": gen1_filled_text(String(header["before"]))}, { "type": &"request", "values": { @@ -5915,6 +5935,7 @@ func _gen1_trainer_steps(row: Dictionary, event: Dictionary) -> Array: "trainer_flag": flag, "object_index": int(event.get("object_index", -1)), "standing_wild": not event.has("trainer_class"), + "end_script": header.get("end_script", []), }, ] @@ -6286,6 +6307,7 @@ func _gen1_step(type: StringName) -> Dictionary: ## row writes takes no turn of its own and is spent on the way past, and the ## balance window one of those draws rides on the result behind it. func _gen1_result() -> Array: + _gen1_battle_last() var events: Array = [] while not _gen1_steps.is_empty() and _gen1_written(_gen1_steps[0], events): _gen1_steps.pop_front() @@ -6298,6 +6320,19 @@ func _gen1_result() -> Array: return [result] +## `OverworldLoop` reads `wCurOpponent` once the script has returned, so the +## rest of a row stands in front of its battle: Yellow's initial catch training +## sets its event behind the store and the throw reads it. +func _gen1_battle_last() -> void: + var battles: Array = [] + var rest: Array = [] + for step: Dictionary in _gen1_steps: + var request: Dictionary = step.get("values", {}) if step.get("type", &"") == &"request" else {} + (battles if StringName(request.get("kind", &"")) == &"battle_requested" else rest).append(step) + if not battles.is_empty() and not rest.is_empty(): + _gen1_steps = rest + battles + + ## `SetLastBlackoutMap`, whose whole body is the rest-house list: healing in one ## of the Safari Zone's three leaves the map a blackout lands on where it was. ## -1 is no outdoor map walked out of rather than a map id, and records nothing. @@ -6393,6 +6428,9 @@ func _gen1_written(step: Dictionary, events: Array) -> bool: &"map_script": state.set_gen1_map_script(int(step["byte"]), int(step["value"])) return true + &"saved_coord_index": + _gen1_saved_coord_index = int(step["value"]) + return true &"last_map": _gen1_last_map = int(step["map"]) return true @@ -6745,6 +6783,14 @@ func _gen1_battle_won(step: Dictionary, result: Dictionary) -> void: if bool(step.get("standing_wild", false)): gen1_toggle_object(_gen1_toggle_index(int(step.get("object_index", -1))), true) load_object_masks() + ## `TextCommand_ASM` behind the end-battle line the fight printed. + var ending: Array = [] + for node: Dictionary in step.get("end_script", []) as Array: + if String(node.get("op", "")) != "text": + ending.append(node) + var steps: Array = [] + if not ending.is_empty() and _gen1_resolve_script(ending, steps, _gen1_run({})): + _gen1_steps = steps + _gen1_steps ## `CollisionCheckOnLand` skips every test while `wSimulatedJoypadStatesIndex` @@ -6752,7 +6798,7 @@ func _gen1_battle_won(step: Dictionary, result: Dictionary) -> void: func _gen1_walk_player(moves: Array) -> Array: var generated: Array = [] for leg: Dictionary in moves: - var direction: Vector2i = _movement_direction(int(leg["direction"])) + var direction: Vector2i = movement_direction(int(leg["direction"])) for _step: int in int(leg["steps"]): var destination: Vector2i = player_cell + direction if not _cell_in_bounds(destination): @@ -6780,7 +6826,7 @@ func _gen1_walk_object(index: int, moves: Array) -> Array: _gen1_stand_object(index) var facing: int = object.facing for row: int in moves: - var direction: Vector2i = _movement_direction(row) + var direction: Vector2i = movement_direction(row) facing = facing_for_direction(direction) var destination: Vector2i = object.cell + direction if not _cell_in_bounds(destination): @@ -6829,7 +6875,6 @@ func _gen1_toggle_index(object_index: int) -> int: ## Whether the script holding the world is one that stops the map around it. -## ## `ScriptEvents` runs inside `HandleMap`, and `HandleMapObjects` runs on the ## same iteration, so a script standing in `WaitScript` or `WaitScriptMovement` ## leaves every object that is not frozen stepping: `FreezeAllOtherObjects` is @@ -7211,6 +7256,22 @@ func _clear_active_script() -> void: unfreeze_all_objects() +## The object row of [param index] with its live cell, as the events at a cell +## carry it, or {} past the table. +func _gen1_object_event(index: int) -> Dictionary: + var rows: Array = current_map.events.get("objects", []) if current_map != null else [] + if index < 0 or index >= rows.size() or index >= objects.size() \ + or not rows[index] is Dictionary: + return {} + var object: Gen2WorldObject = objects[index] + var event: Dictionary = (rows[index] as Dictionary).duplicate(true) + event["x"] = object.cell.x + event["y"] = object.cell.y + event["kind"] = &"objects" + event["object_index"] = index + return event + + func _active_events_at(cell: Vector2i) -> Array: var out: Array = [] for event: Dictionary in events_at(cell): @@ -7235,12 +7296,7 @@ func _active_events_at(cell: Vector2i) -> Array: or object.index < 0 or object.index >= rows.size() \ or not rows[object.index] is Dictionary: continue - var event: Dictionary = (rows[object.index] as Dictionary).duplicate(true) - event["x"] = object.cell.x - event["y"] = object.cell.y - event["kind"] = &"objects" - event["object_index"] = object.index - out.append(event) + out.append(_gen1_object_event(object.index)) return out @@ -7546,7 +7602,6 @@ func _queue_map_callbacks(callback_type: int) -> void: if current_map == null: return if _gen1: - _unlock_gen1_card_key_door() ## `RunMapScript`'s first frame behind `EnterMap`: its writes stand before a ## step, its boxes wait for one, and `CheckAndResetEvent` answers only once. _gen1_entry_steps = _spend_gen1_nodes(_gen1_map_script_nodes(Gen1Layout.MAP_LOAD_BOTH)) @@ -7567,14 +7622,26 @@ func _queue_map_callbacks(callback_type: int) -> void: ## The map's script with [param mask] standing in `wCurrentMapScriptFlags`. func _gen1_map_script_nodes(mask: int) -> Array: + if mask & (1 << Gen1Layout.MAP_LOADED_1_BIT): + _unlock_gen1_card_key_door() + var callback: Dictionary = _gen1_map_load_callback(mask) + return callback["nodes"] as Array if not callback.is_empty() \ + else current_map.scripts.get("entry", []) as Array + + +func _gen1_map_load_callback(mask: int) -> Dictionary: for callback: Dictionary in current_map.scripts.get("callbacks", []) as Array: if int(callback.get("mask", 0)) == mask: - return callback["nodes"] as Array - return current_map.scripts.get("entry", []) as Array + return callback + return {} +## Whether the next pass owes the map a body its own script does not run every +## frame. `CinnabarIsland_Script` sets BIT_CUR_MAP_LOADED_1 on every frame and +## nothing on the island reads it, so a bit no callback takes is not pending. func gen1_map_load_pending() -> bool: - return _gen1_map_load_pending != 0 + return _gen1_map_load_pending != 0 and current_map != null \ + and not _gen1_map_load_callback(_gen1_map_load_pending).is_empty() ## Spends every step of [param nodes] that takes no turn of its own, stopping @@ -7590,8 +7657,8 @@ func _spend_gen1_nodes(nodes: Array) -> Array: ## `_SetCardKeyDoorYScript` and `_UnlockedDoorEventScript`, which run -## in front of the blocks: the door `PrintCardKeyText` last opened becomes that -## door's own flag, and the coordinates are cleared on the floor that owns them. +## in front of the blocks on every BIT_CUR_MAP_LOADED_1 pass: the door +## `PrintCardKeyText` last opened becomes that door's own flag. func _unlock_gen1_card_key_door() -> void: if state == null: return @@ -8005,12 +8072,12 @@ func _apply_object_movement(event: Dictionary) -> Array: if kind in SCRIPTED_TURN_KINDS: ## Queued rather than applied, so a turn behind a walk turns where ## the walk ends. `queue_step` drains an entry of no frames itself. - var turn: Vector2i = _movement_direction(int(command.get("direction", 0))) + var turn: Vector2i = movement_direction(int(command.get("direction", 0))) object.queue_step(Vector2i.ZERO, 0, false, turn) final_facing = facing_for_direction(turn) continue if SCRIPTED_STEP_PASSES.has(kind): - var direction: Vector2i = _movement_direction(int(command.get("direction", 0))) + var direction: Vector2i = movement_direction(int(command.get("direction", 0))) var jumping: bool = kind in JUMP_STEP_KINDS var cells: int = 2 if jumping else 1 var destination: Vector2i = object.cell + direction * cells @@ -8139,11 +8206,11 @@ func _apply_player_movement(event: Dictionary) -> Array: ## the walk ends rather than on the frame the stream was applied. _queue_player_step( Vector2i.ZERO, 0, false, - _movement_direction(int(command.get("direction", 0))), kind, + movement_direction(int(command.get("direction", 0))), kind, ) continue if SCRIPTED_STEP_PASSES.has(kind): - var direction: Vector2i = _movement_direction(int(command.get("direction", 0))) + var direction: Vector2i = movement_direction(int(command.get("direction", 0))) var jumping: bool = kind in JUMP_STEP_KINDS var cells: int = 2 if jumping else 1 var destination: Vector2i = player_cell + direction * cells @@ -8192,7 +8259,7 @@ func _apply_player_movement(event: Dictionary) -> Array: return generated -func _movement_direction(direction: int) -> Vector2i: +func movement_direction(direction: int) -> Vector2i: match direction & 3: 0: return Vector2i.DOWN @@ -8864,7 +8931,6 @@ func _object_landing_cells( ## Advances the movement templates whose source behavior is data-driven in this ## slice. Scripted movement is executed by the script runner, while followers ## advance after each successful player step. -## ## One decision per eligible object per call. A caller wanting the source's ## pacing uses advance_object_steps_pass(), which spends the durations. func advance_objects(random: RandomNumberGenerator) -> int: @@ -8956,7 +9022,6 @@ func _object_stays_on_screen(destination: Vector2i) -> bool: ## One hardware frame of the data-driven movement templates. -## ## An object spends a frame of an in-flight step, a frame of its wait, or takes ## a decision: STEP_TYPE_CONTINUE_WALK, _SLEEP and _FROM_MOVEMENT. The cell ## commits when the step starts, as InitStep does, and the offset trails. @@ -9058,7 +9123,6 @@ func advance_scripted_steps_pass() -> bool: ## Spends the frames a script is waiting on and resumes it the frame its wait ## ends, returning whatever that produced. -## ## The two waits are `ScriptEvents`'s own: SCRIPT_WAIT_MOVEMENT, which ends when ## the stream an `applymovement` started has been drawn, and the counted delay ## `pause`, `wait`, `deactivatefacing` and `showemote` spend. A host calls this @@ -10730,7 +10794,6 @@ func basement_key_request() -> Dictionary: ## `wItemEffectSucceeded` is set before the script is queued, and ## `.CheckCanUseSquirtbottle` only picks which half of it runs. So the refusal is ## the queued script's own `_SquirtbottleNothingText` rather than `.Oak`. -## ## The test is `GetFacingObject` and `cp SPRITEMOVEDATA_SUDOWOODO`, the same ## shape rock_smash_request() uses, behind the Route 36 map check. func squirtbottle_request() -> Dictionary: diff --git a/game/world/world_bag_host.gd b/game/world/world_bag_host.gd index 9a5fa03c..426fbe2a 100644 --- a/game/world/world_bag_host.gd +++ b/game/world/world_bag_host.gd @@ -10,7 +10,6 @@ extends RefCounted ## stacks. The commit boundary is [Gen2WorldTransaction]. ## `TossItem` with `wCurItemQuantity`, as one validated transaction. -## ## `TossMenu` is only reachable from a submenu that offered TOSS, so the ## permission is already settled by the time the source gets here; it is checked ## again because a caller is not always that menu. @@ -223,7 +222,6 @@ static func register( ## `SwitchItemsInBag` as a committed transaction: one pocket's rows put in ## [param pocket_order], with the rest of the bag left where it sits. -## ## The caller has already run [method Gen2WorldPack.switch_items] to work out the ## order; this only writes it. `wPCItems` is the same routine over a different ## list, which [param pc] selects. diff --git a/game/world/world_battle.gd b/game/world/world_battle.gd index 1f0d4c5b..68c20f09 100644 --- a/game/world/world_battle.gd +++ b/game/world/world_battle.gd @@ -37,7 +37,7 @@ static func prepare( battle_rules: Gen2Rules = null, player_id: int = -1, ) -> Dictionary: - if data == null or player_party == null: + if data == null: return _failure(&"missing_player_party") var raw_values: Variant = request.get("values", request) @@ -45,6 +45,9 @@ static func prepare( return _failure(&"invalid_battle_request") var values: Dictionary = (raw_values as Dictionary).duplicate(true) var kind: StringName = StringName(values.get("kind", &"")) + player_party = _tutor_stand_in(data, values, player_party) + if player_party == null: + return _failure(&"missing_player_party") if kind == &"battle_tower": for member: Gen2BattleMon in player_party.mons: member.restore_health() @@ -118,6 +121,14 @@ static func prepare( } +## Yellow's `.checkAnyPartyAlive` skips `AnyPartyAlive` for the Pikachu battle, +## fought before the player owns a Pokemon; a tutor sends nobody out. +static func _tutor_stand_in(data: GameData, values: Dictionary, party: Gen2Party) -> Gen2Party: + if bool(values.get("tutorial", false)) and (party == null or party.is_wiped()): + return fallback_party(data) + return party + + static func _recorded_party(data: GameData, values: Dictionary, tower: bool) -> Gen2Party: var members: Array = [] for raw_mon: Variant in values.get("enemy_party", []) as Array: diff --git a/game/world/world_bug_contest.gd b/game/world/world_bug_contest.gd index e698f123..01d3114b 100644 --- a/game/world/world_bug_contest.gd +++ b/game/world/world_bug_contest.gd @@ -4,7 +4,6 @@ extends RefCounted ## The Bug Catching Contest: its own encounter roll, its score, and its judging ## (`engine/events/bug_contest/`, `engine/overworld/events.asm`'s ## `TryWildEncounter_BugContest` and `ChooseWildEncounter_BugContest`). -## ## Scene-free and stateless, like [Gen2WorldTreemon]: the tables are the cache's ## (`GameData.bug_contest_mons`, `GameData.bug_contestants`), the live counters ## are [Gen2WorldState]'s, and every roll takes the caller's generator so a @@ -137,7 +136,6 @@ static func _level(row: Dictionary, random: RandomNumberGenerator) -> int: ## `ContestScore`: the tally the player's caught Pokemon is judged on. Every ## term is the *low* byte of a big-endian word, which is the stat itself for ## anything a contest Pokemon can reach, and the whole runs 16 bit with carry. -## ## [param mon] is the caught Pokemon as [Gen2WorldState] keeps it: ## `max_hp`, `hp`, `attack`, `defense`, `speed`, `special_attack`, ## `special_defense`, `dvs` and `item`. @@ -174,7 +172,6 @@ static func _dv_score(dvs: int) -> int: ## `BugContest_JudgeContestants`: the AI contestants are scored first and the ## player is inserted last, which is why the player takes a place on a tie. -## ## [param withdrawn] is the set of contestant indices whose event flag is set, ## which `SelectRandomBugContestContestants` chose and who therefore do not ## compete. Answers `{"placings": [...], "player_place": 0..3}` with the placings diff --git a/game/world/world_effects.gd b/game/world/world_effects.gd index d5f3830e..5096feae 100644 --- a/game/world/world_effects.gd +++ b/game/world/world_effects.gd @@ -224,7 +224,6 @@ func start_headbutt_tree(cell: Vector2i) -> void: ## the splitting tree and index 1 the four leaves (`Gen2WorldFieldMove`'s ## ANIMATION_TREE and ANIMATION_GRASS), which is the byte ## `CheckOverworldTileArrays` returns beside the replacement block. -## ## [param player_cell] is where the player stands, which is what picks the ## leaves' own corner of the block. func start_cut( @@ -305,7 +304,6 @@ func start_boulder_dust(object_index: int, cell: Vector2i, direction: Vector2i, ## sprite: its OAM is written at fixed screen pixels while the script waits, and ## it wears `gfx/overworld/heal_machine.pal` over `wOBPals2`' PAL_OW_TREE slot ## rather than an overworld palette, which is what `palette: -1` says here. -## ## [param balls] is `wPartyCount`; the source returns before writing anything ## when it is zero. func start_heal_machine( diff --git a/game/world/world_palette.gd b/game/world/world_palette.gd index 9dc8de73..7d9562d6 100644 --- a/game/world/world_palette.gd +++ b/game/world/world_palette.gd @@ -36,7 +36,6 @@ const PALETTE_MASK: int = 0x07 ## Which of the four palette rows a map draws with right now. -## ## `ReplaceTimeOfDayPals` and `GetTimePalette` together. A ## [constant PALETTE_DARK] map is the only one the clock cannot reach: it is ## [constant TIME_DARK] until Flash has been used and [constant TIME_NIGHT] @@ -173,7 +172,6 @@ static func palette_slots(environment: int, time_of_day: int) -> Array: ## The two colours a map group lends `PAL_BG_ROOF`, or nothing when the map is ## not one `_LoadMapPals` reaches the roof branch for. -## ## `cp NITE_F / jr c, .morn_day` is the whole test, so DARKNESS takes the nite ## pair as well: the four rows are two, not four. static func roof_colors( @@ -250,7 +248,6 @@ static func _flood_white(resolved: Array) -> void: ## One palette per tile of [param tileset], in tile order. -## ## Every tile of the strip shares eight palette slots, so the eight are resolved ## once and the same one is handed to every tile that uses it. This runs again on ## every frame an animated tileset changes a tile, and building one palette copy diff --git a/game/world/world_party_host.gd b/game/world/world_party_host.gd index d2924d44..d6057c7c 100644 --- a/game/world/world_party_host.gd +++ b/game/world/world_party_host.gd @@ -1289,12 +1289,13 @@ static func capture_wild( return _failure(&"missing_capture_context", {}) ## The Dude's throw: `.catch_without_fail` in front of every multiplier, and ## `.return_from_capture`'s `cp BATTLETYPE_TUTORIAL / ret z` behind the line. - ## The ball is his and nothing is kept, so no transaction opens. + ## The ball is his and nothing is kept, so no transaction opens. Generation + ## 1's `.oldManBattle` ends the battle even on the throw Yellow refuses. if battle_type == Gen2Battle.BATTLETYPE_TUTORIAL: return { - "ok": true, "caught": true, "ball": ball, "wobbles": 3, + "ok": true, "caught": world.gen1_tutorial_ball_lands(), "ball": ball, "wobbles": 3, "catch_rate": 255, "species": wild.species, - "tutorial": true, "persistent": false, + "tutorial": true, "persistent": false, "ends_battle": true, } var opened: Dictionary = Gen2WorldTransaction.begin(world, save) if not bool(opened.get("ok", false)): diff --git a/game/world/world_radio.gd b/game/world/world_radio.gd index dd86a88c..d2a0d03d 100644 --- a/game/world/world_radio.gd +++ b/game/world/world_radio.gd @@ -96,7 +96,6 @@ const CHANNELS: Array[Dictionary] = [ ## Let's All Sing's, though only `PlayRadioStationPointers` reaches it and a map ## radio prints no name. `LoadStation_BuenasPassword` answers with ## `NotBuenasPasswordName`, an empty string, until Team Rocket takes the tower. -## ## `.returnafterstation` places this before `PlayRadioShow` runs, so the name is ## the station the dial is on and never the Rocket broadcast standing in for it. const STATION_NAMES: Dictionary = { @@ -164,7 +163,6 @@ static func is_kanto_landmark(landmark: int, crystal: bool = true) -> bool: ## The station a knob position answers with, or a no-signal result. -## ## [param context] carries what the source reads off WRAM: `landmark` (already ## resolved through GetWorldMapLocation), `crystal`, `expn_card`, ## `rocket_signal`, `rockets_in_radio_tower` and `time_of_day`. diff --git a/game/world/world_screen.gd b/game/world/world_screen.gd index eeb36f43..7c6874ad 100644 --- a/game/world/world_screen.gd +++ b/game/world/world_screen.gd @@ -4863,8 +4863,20 @@ func preview_meet_visible_encounter(cell: Vector2i) -> bool: ## development Master Ball, starts an imported wild encounter, and leaves the ## production battle overlay on its throw message. ## `CatchTutorial`: the Dude's own fight, which answers itself. `Route29Tutorial1` -## loads the same `loadwildmon RATTATA, 5` in front of it. -func preview_catch_tutorial() -> void: +## loads the same `loadwildmon RATTATA, 5` in front of it. A Generation 1 cache +## throws the old man's, or Prof. Oak's for [param pikachu]. +func preview_catch_tutorial(pikachu: bool = false) -> void: + if _data != null and _data.generation == RomRegistry.GEN1: + var raw: int = Gen1Layout.BATTLE_TYPE_PIKACHU if pikachu else Gen1Layout.BATTLE_TYPE_OLD_MAN + var wilds: Dictionary = Gen1Layout.TUTOR_WILDS.get(_data.id, {}) + if not wilds.has(raw): + return + var values: Dictionary = { + "kind": &"wild", "pokemon": int(wilds[raw]), "level": Gen1Layout.TUTOR_WILD_LEVEL, + } + values.merge(Gen1Layout.battle_type_values(raw)) + _start_battle_request({"kind": &"battle_requested", "values": values}) + return _start_battle_request({ "kind": &"catch_tutorial_requested", "values": { @@ -6272,6 +6284,9 @@ func _on_credits_closed() -> void: _renderer.refresh() _script_prompt = "" _refresh_labels() + if _world != null and StringName(_world.pending_runtime_request().get("kind", &"")) \ + == &"hall_of_fame_requested": + _show_script_results(_world.complete_runtime_request({"ok": true})) ## `.music`, whose `PlayMusic MUSIC_NONE` and `DelayFrame` in front of the real @@ -7691,6 +7706,8 @@ const REQUEST_HANDLERS: Dictionary = { &"swarm_requested": &"_request_swarm", &"map_radio_requested": &"_request_map_radio", &"audio_requested": &"_request_audio", + &"hall_of_fame_requested": &"_request_hall_of_fame", + &"soft_reset_requested": &"_request_soft_reset", } ## Runtime requests the screen answers by opening a page: the method that opens @@ -8057,6 +8074,22 @@ func _request_bug_contest_judging(_request: Dictionary) -> StringName: ## `TryQuickSave`, which is `Link_SaveGame`: the overwrite question, the SAVING ## box and `SavedTheGame`, on the service screen where BILL'S PC's already are. A ## driver with no scene behind it still writes rather than hanging. +## `HallOfFamePC`, which Generation 1's own room script calls and waits on: the +## induction and the credits, and the script's save and reset behind them run +## when the credits close. +func _request_hall_of_fame(_request: Dictionary) -> StringName: + open_hall_of_fame() + return &"break" + + +## `jp Init` at the end of `HallOfFameResetEventsAndSaveScript`, a request where +## Crystal's `special Reset` is an event. +func _request_soft_reset(_request: Dictionary) -> StringName: + persist_world_snapshot() + _soft_reset() + return &"break" + + func _request_quick_save(_request: Dictionary) -> StringName: if _service_host == null and _open_quick_save_screen(): return &"break" diff --git a/game/world/world_script.gd b/game/world/world_script.gd index 32636675..e1248484 100644 --- a/game/world/world_script.gd +++ b/game/world/world_script.gd @@ -2,7 +2,6 @@ class_name Gen2WorldScript extends RefCounted ## Shared Generation 2 overworld script command definitions. -## ## The cartridge stores one command byte followed by command-specific operands. ## This file describes the byte layout for the commands used by the bounded ## overworld runner. Unknown commands remain visible to the caller instead of @@ -554,7 +553,6 @@ const NON_RETURNING_SOURCE_OPCODES: Array[int] = [ ## Whether the command at [param opcode] is followed by another command. -## ## A linear walk over a script stops here, not at [method is_terminal]: a script ## that ends in `jumptext` is followed by the text it named, and a `jumpstd` ## table by its pointers. Only the live runner, which dispatches one command at @@ -945,7 +943,6 @@ static func decode_command_queue_entry(data: PackedByteArray) -> Dictionary: ## Decodes a `stonetable`, the only cmdqueue payload either game ships. -## ## Rows are `warp_id, object_id, script` until a $ff warp id, and the ids are ## the source's own: the warp is one-based, as `.check_on_warp` counts it, and ## the object is an `object_const_def` constant, which starts at 2, so it is two diff --git a/game/world/world_script_runner.gd b/game/world/world_script_runner.gd index 3a892a66..dcd17bd4 100644 --- a/game/world/world_script_runner.gd +++ b/game/world/world_script_runner.gd @@ -2,7 +2,6 @@ class_name Gen2WorldScriptRunner extends RefCounted ## Bounded, scene-free execution of the supported overworld script commands. -## ## A runner owns one event invocation. It never opens a ROM and it never ## changes the world state until the invocation reaches END or ENDCALLBACK. ## Text and explicit warps are returned as structured pauses for the screen or @@ -2710,7 +2709,6 @@ func _command_checkpokemail(_opcode: int, command: Dictionary, _bank: int) -> Di ## A command whose numbers a mod may have moved, substituted before it runs. -## ## The site is addressed by the byte it sits at, `frame.address + offset`, which ## is exactly the id [Gen2WorldCatalog] gave it. Only the OPERANDS change: the ## command still runs, its script still sets its own completion flag, prints its @@ -4121,7 +4119,6 @@ func _var_kenji_break_timer() -> Variant: ## store of wScriptVar into the address it answered. Only those rows can be ## written: every other entry hands back a copy in wStringBuffer2 or runs a ## routine, so a `writevar` naming one writes nothing the script can read back. -## ## Every `writevar` in either game is RadioTower2F's own award of a Blue Card ## point, which stopped the script here until this existed. func _write_runtime_variable(variable: int) -> Dictionary: @@ -5864,7 +5861,6 @@ func _battle_tower_party() -> Dictionary: ## `_CheckForBattleTowerRules`: every rule is run rather than stopping at the ## first failure, so a party can fail more than one and the receptionist says so ## once per failure. `BattleTower_PleaseReturnWhenReady` closes the run. -## ## wScriptVar is TRUE when something failed, which is what ## `ifnotequal FALSE, Script_WaitButton` refuses the challenge on. func _check_battle_tower_rules() -> Dictionary: @@ -6632,7 +6628,6 @@ func _byte_array(raw: Variant) -> PackedByteArray: ## `ReadCaughtData` and `SeerAction`, which are one reading of the row and then ## the boxes that reading picked. -## ## Nothing here writes anything: every branch is a run of `PrintText`s and the ## five buffers they read, so the whole routine is text. func _finish_poke_seer(special: int, result: Dictionary) -> Dictionary: @@ -6822,7 +6817,6 @@ func _stage_item_ball() -> Dictionary: ## `FruitTreeScript` (`engine/events/fruit_trees.asm`). Like an item ball it is ## a script rather than a host request: `fruittree` is the whole of the object's ## own script, and the routine below it is text, a flag and a `giveitem`. -## ## The first pause is `FruitBearingTreeText`; `TryResetFruitTrees` and ## `CheckFruitTree` run on its acknowledge, since the source's own `callasm`s sit ## behind the `promptbutton`. @@ -7312,7 +7306,6 @@ func text_context() -> Dictionary: ## The buffers this runner filled, keyed the way `TextCommand_RAM` asks for them. -## ## `getstring` and `verbosegiveitem` fill buffers by `text_buffer` number, but a ## `text_ram` names the same storage by its WRAM address, so the item texts read ## through StringBufferPointers rather than the number. Addresses come from the @@ -7435,7 +7428,6 @@ func _engine_flag_active(flag: int) -> bool: ## _GetVarAction's .CountBadges, over staged flags rather than committed ones. -## ## The cartridge has no staging: `setflag` writes wBadges and the `readvar` ## after it reads what was just written. Mahogany Gym is where that matters: ## PryceScript sets ENGINE_GLACIERBADGE, reads VAR_BADGES and branches on 7 to diff --git a/game/world/world_tileset.gd b/game/world/world_tileset.gd index 99335e58..ed53edb4 100644 --- a/game/world/world_tileset.gd +++ b/game/world/world_tileset.gd @@ -2,7 +2,6 @@ class_name Gen2WorldTileset extends RefCounted ## Runtime data for one overworld tileset. -## ## Graphics are kept in the cache as an indexed tile strip and loaded lazily by ## GameData. The metatile and collision tables stay here because map expansion ## needs them without opening the cartridge. @@ -90,7 +89,6 @@ func collision_index(block: int, cell_x: int, cell_y: int) -> int: ## Which of the eight background palette slots a tile of the strip draws with. -## ## Palette maps store two assignments per byte, low nibble first, over the whole ## 224-tile span. Bit 3 of a nibble is the VRAM bank the cartridge draws that ## tile from and is dropped here: the strip is flat, so the tile number already diff --git a/tests/unit/test_gen1_layout.gd b/tests/unit/test_gen1_layout.gd index 6885fe8b..209b6992 100644 --- a/tests/unit/test_gen1_layout.gd +++ b/tests/unit/test_gen1_layout.gd @@ -592,3 +592,36 @@ func test_the_two_map_free_rods_carry_their_own_slots() -> void: assert_eq(good_rod.size(), 2) for slot: Dictionary in good_rod: assert_eq(int(slot["level"]), 10) + + +## `wBattleType` read across: the two tutors are Crystal's tutorial with the +## raw byte kept, the Safari is its own, and a value no script writes adds nothing. +func test_a_battle_type_byte_reads_across_onto_crystals() -> void: + var old_man: Dictionary = Gen1Layout.battle_type_values(Gen1Layout.BATTLE_TYPE_OLD_MAN) + assert_eq(int(old_man["battle_type"]), Gen2Battle.BATTLETYPE_TUTORIAL) + assert_true(bool(old_man["tutorial"])) + assert_eq(int(old_man["gen1_battle_type"]), Gen1Layout.BATTLE_TYPE_OLD_MAN) + var pikachu: Dictionary = Gen1Layout.battle_type_values(Gen1Layout.BATTLE_TYPE_PIKACHU) + assert_eq(int(pikachu["battle_type"]), Gen2Battle.BATTLETYPE_TUTORIAL) + assert_eq( + int(Gen1Layout.battle_type_values(Gen1Layout.BATTLE_TYPE_SAFARI)["battle_type"]), + Gen2Battle.BATTLETYPE_SAFARI + ) + assert_false(Gen1Layout.battle_type_values(Gen1Layout.BATTLE_TYPE_SAFARI).has("tutorial")) + assert_true(Gen1Layout.battle_type_values(0).is_empty()) + + +## `.oldManBattle`: only Yellow's old man can miss, and only under its event. +func test_the_tutors_ball_lands_unless_yellows_initial_training_stands() -> void: + var set_flag: Callable = func(_flag: int) -> bool: return true + var clear_flag: Callable = func(_flag: int) -> bool: return false + assert_true(Gen1Layout.tutorial_ball_lands(RomRegistry.RED, Gen1Layout.BATTLE_TYPE_OLD_MAN, set_flag)) + assert_true(Gen1Layout.tutorial_ball_lands(RomRegistry.YELLOW, Gen1Layout.BATTLE_TYPE_PIKACHU, set_flag)) + assert_true(Gen1Layout.tutorial_ball_lands(RomRegistry.YELLOW, Gen1Layout.BATTLE_TYPE_OLD_MAN, clear_flag)) + assert_false(Gen1Layout.tutorial_ball_lands(RomRegistry.YELLOW, Gen1Layout.BATTLE_TYPE_OLD_MAN, set_flag)) + + +## Red and Blue draw the player and the old man; Yellow adds Prof. Oak. +func test_yellow_alone_carries_prof_oaks_back_pic() -> void: + assert_eq(Gen1Layout.player_backpics(Gen1Layout.RED_BLUE), ["player", "old_man"]) + assert_eq(Gen1Layout.player_backpics(Gen1Layout.YELLOW), ["player", "old_man", "prof_oak"]) diff --git a/tests/unit/test_launcher_ui.gd b/tests/unit/test_launcher_ui.gd index ac4822a3..bd6aacd7 100644 --- a/tests/unit/test_launcher_ui.gd +++ b/tests/unit/test_launcher_ui.gd @@ -746,26 +746,25 @@ func test_ejecting_a_cartridge_empties_its_bay() -> void: assert_almost_eq(card.position.y, card.rest_y(), 0.5, "and back where it stood") -## A generation with an importer and no world reads its cartridge and cannot -## start it, so the bar must not offer Play on a seated one. -func test_a_seated_but_unplayable_cartridge_offers_nothing_to_press() -> void: +## Every registry cartridge walks to its Hall of Fame, so a seated one of +## either generation offers Play, and an empty bay still takes a dump. +func test_a_seated_cartridge_of_either_generation_offers_play() -> void: var page: Gen2ShelfPage = Gen2ShelfPage.create(_light, false) add_child_autofree(page) page.size = Vector2(900, 600) await get_tree().process_frame - # Unanimated, because the depth a bay reads is only settled once the slide is. - page.stage().select(RomRegistry.ORDER.find(RomRegistry.RED), false) - await get_tree().process_frame - - page.set_slot_state(RomRegistry.RED, RomCache.STATE_MISSING, "") - assert_eq(page.hints()[0]["label"], "Add cartridge", "an empty bay still takes a dump") - - page.set_slot_state(RomRegistry.RED, RomCache.STATE_USABLE, "Ready") - var labels: Array = [] - for entry: Dictionary in page.hints(): - labels.append(entry["label"]) - assert_false(labels.has("Play"), "Red cannot be played yet") - assert_true(labels.has("Options"), "and its cache is still reachable") + for id: StringName in [RomRegistry.RED, RomRegistry.GOLD]: + # Unanimated, because the depth a bay reads is only settled once the slide is. + page.stage().select(RomRegistry.ORDER.find(id), false) + await get_tree().process_frame + page.set_slot_state(id, RomCache.STATE_MISSING, "") + assert_eq(page.hints()[0]["label"], "Add cartridge", "an empty bay still takes a dump") + page.set_slot_state(id, RomCache.STATE_USABLE, "Ready") + var labels: Array = [] + for entry: Dictionary in page.hints(): + labels.append(entry["label"]) + assert_true(labels.has("Play"), "%s can be played" % id) + assert_true(labels.has("Options"), "and its cache is still reachable") ## A cache an older build wrote is not an empty bay. The player imported this diff --git a/tests/unit/test_source_budget.gd b/tests/unit/test_source_budget.gd index 7b19ae4b..a9102fd2 100644 --- a/tests/unit/test_source_budget.gd +++ b/tests/unit/test_source_budget.gd @@ -21,7 +21,7 @@ const MAX_COMMENT_BLOCK: int = 8 ## the shared code, the largest of them 152 the region map, 150 the battle ## animation engine, 140 the transition, 131 the field moves, 130 the Pokedex, ## 108 the three PCs, 90 the Safari Zone, 52 the menus a row draws. -const MAX_COMMENT_LINES: int = 41986 +const MAX_COMMENT_LINES: int = 41932 ## What no comment block ever ends on. A pass that meets the ceiling above trims ## the second line of a two-line block and leaves the first mid-sentence, which diff --git a/tests/unit/test_world_api.gd b/tests/unit/test_world_api.gd index d7038baf..023b4170 100644 --- a/tests/unit/test_world_api.gd +++ b/tests/unit/test_world_api.gd @@ -10103,3 +10103,26 @@ func test_gen1_script_branches_read_pending_flags_and_scratch_in_each_choice() - assert_eq(steps[0]["no"], [{"type": &"text", "text": "NO"}]) assert_false(world.state.is_event_flag_active(683), "planning a choice writes no save flags") RomCache.clear(_gen1_directory()) + + +## `OverworldLoop` reads `wCurOpponent` after the script has returned, so a +## flag the row sets behind the store is set before the fight opens, and the +## `wBattleType` byte rides the request read across onto Crystal's numbering. +func test_gen1_a_rows_battle_stands_behind_the_rest_of_the_row() -> void: + var world: Gen2WorldAPI = _gen1_world(0, Vector2i(1, 2)) + world._gen1_steps = world._gen1_script_steps({"script": [ + {"op": "wild_battle", "species": 19, "level": 5, + "battle_type": Gen1Layout.BATTLE_TYPE_OLD_MAN}, + {"op": "flag", "flag": 47, "set": true}, + ]}) + var results: Array = world._gen1_result() + assert_true(world.state.is_event_flag_active(47), "the flag behind the store is set first") + var request: Dictionary = world.pending_runtime_request() + assert_eq(StringName(request.get("kind", &"")), &"battle_requested") + var values: Dictionary = request.get("values", {}) + assert_eq(int(values.get("battle_type", -1)), Gen2Battle.BATTLETYPE_TUTORIAL) + assert_true(bool(values.get("tutorial", false))) + assert_eq(int(values.get("gen1_battle_type", -1)), Gen1Layout.BATTLE_TYPE_OLD_MAN) + assert_true(world.gen1_tutorial_ball_lands()) + assert_eq(StringName((results[0] as Dictionary).get("status", &"")), &"waiting") + RomCache.clear(_gen1_directory()) diff --git a/tools/checks/gen1_battle.gd b/tools/checks/gen1_battle.gd index e3f21b49..cda39648 100644 --- a/tools/checks/gen1_battle.gd +++ b/tools/checks/gen1_battle.gd @@ -68,6 +68,7 @@ func _one_game() -> void: _teleport_ends_the_battle() _the_bag_in_a_fight() _a_safari_battle() + _the_tutor_throws() ## `AddPartyMon`'s four base moves and `WriteMonMoves` over them, for every @@ -533,3 +534,103 @@ func _safari_run_roll() -> void: ran[index] += 1 _r.check(ran[1] < ran[0] and ran[0] < ran[2], "the run rolls came out %s eating, plain and angry." % [ran]) + + +## The tutor on the real overlay, frame by frame, from the map script that +## starts him: Viridian City's catch training, Yellow's initial one that +## `ItemUseBall` refuses, and Prof. Oak's PIKACHU with no party at all. A row is +## the state to start on, the one left behind, the wild, and whether the ball lands. +const TUTOR_ROWS: Dictionary = { + &"red": [[1, 2, 13, true]], &"blue": [[1, 2, 13, true]], + &"yellow": [[3, 4, 19, true], [7, 8, 19, false]], +} +const TUTOR_GUARD_FRAMES: int = 6000 +const VIRIDIAN_CITY: int = 1 +const VIRIDIAN_BYTE: int = 4 +const PALLET_TOWN: int = 0 +const TUTOR_CELL := Vector2i(23, 10) +const PIKACHU_DEX: int = 25 +## `_ItemUseText001`, `text_low`, `_ItemUseText002`; `_ItemUseBallText05`'s +## `line` and `cont`; and `_ItemUseBallText04`. +const TUTOR_USED: String = "%s used\nPOKé BALL!" +const TUTOR_CAUGHT: String = "All right!\n%s was" + Gen2TextStream.SCROLL_BREAK + "caught!" +const TUTOR_BROKE_FREE: String = "Shoot! It was so\nclose too!" + + +func _the_tutor_throws() -> void: + for row: Array in TUTOR_ROWS[_r.game_id] as Array: + var screen: Gen2WorldScreen = _open_screen(VIRIDIAN_CITY, TUTOR_CELL, true) + var world: Gen2WorldAPI = screen.world() + var party_before: int = screen.active_save().party.size() + var balls_before: int = world.state.item_quantity(Gen1Layout.ITEM_POKE_BALL) + world.state.set_gen1_map_script(VIRIDIAN_BYTE, int(row[0])) + screen._show_script_results(world.dispatch_sight_events()) + var training: Dictionary = _drive_tutor(screen) + var name: String = String(_r.data.species(int(row[2])).get("name", "")) + _r.check(training["frames"] < TUTOR_GUARD_FRAMES, "the old man's battle never ended: %s" % [training]) + _r.check((training["messages"] as Array).has(TUTOR_USED % "OLD MAN"), "the old man threw nothing: %s" % [training]) + var landed: String = TUTOR_CAUGHT % name if bool(row[3]) else TUTOR_BROKE_FREE + _r.check((training["messages"] as Array).has(landed), "the old man's %s ball said %s" % [name, training]) + _r.check(world.state.gen1_map_script(VIRIDIAN_BYTE) == int(row[1]), + "the training left Viridian City on state %d." % world.state.gen1_map_script(VIRIDIAN_BYTE)) + _r.check(screen.active_save().party.size() == party_before + and world.state.item_quantity(Gen1Layout.ITEM_POKE_BALL) == balls_before, + "the old man's catch was kept.") + _r.note("gen1 tutor: the old man's %s in %d frames, %s" % [ + name, training["frames"], "caught" if bool(row[3]) else "broke free"]) + _close_screen(screen) + if not Gen1Layout.TUTOR_WILDS[_r.game_id].has(Gen1Layout.BATTLE_TYPE_PIKACHU): + return + var lab: Gen2WorldScreen = _open_screen(PALLET_TOWN, TUTOR_CELL, false) + lab.preview_catch_tutorial(true) + var oak: Dictionary = _drive_tutor(lab) + var pikachu: String = String(_r.data.species(PIKACHU_DEX).get("name", "")) + _r.check(oak["frames"] < TUTOR_GUARD_FRAMES, "Prof. Oak's battle never ended: %s" % [oak]) + _r.check((oak["messages"] as Array).has(TUTOR_USED % "PROF.OAK") + and (oak["messages"] as Array).has(TUTOR_CAUGHT % pikachu), "Prof. Oak's throw said %s" % [oak]) + _r.check(lab.active_save().party.is_empty(), "Prof. Oak's PIKACHU joined the party.") + _r.note("gen1 tutor: Prof. Oak's %s in %d frames" % [pikachu, oak["frames"]]) + _close_screen(lab) + + +func _open_screen(map: int, cell: Vector2i, party: bool) -> Gen2WorldScreen: + var screen: Gen2WorldScreen = (load("res://game/world/world_screen.tscn") as PackedScene).instantiate() + screen.map_group = 0 + screen.map_number = map + screen.start_cell = cell + screen.encounter_seed = 1 + screen.set_data(_r.data) + var save: Gen2SaveData = Gen2SaveStore.create_development_save(_r.data, 0) + if not party: + save.party = [] + screen.set_save(save) + (Engine.get_main_loop() as SceneTree).root.add_child(screen) + screen.set_process(false) + return screen + + +func _close_screen(screen: Gen2WorldScreen) -> void: + (Engine.get_main_loop() as SceneTree).root.remove_child(screen) + screen.free() + + +func _drive_tutor(screen: Gen2WorldScreen) -> Dictionary: + var messages: Array[String] = [] + var frames: int = 0 + var host: Gen2BattleScreen = null + while frames < TUTOR_GUARD_FRAMES: + frames += 1 + screen.advance_frame() + var current: Gen2BattleScreen = screen.get("_battle_host") + if host == null: + host = current + continue + if current == null: + break + var snapshot: Dictionary = host.battle_snapshot() + var line: String = String(snapshot.get("message", "")) + if messages.is_empty() or messages.back() != line: + messages.append(line) + if bool(snapshot.get("awaits_press", false)): + screen.press_button(PokeButton.A) + return {"frames": frames, "messages": messages} diff --git a/tools/checks/gen1_maps.gd b/tools/checks/gen1_maps.gd index 4773a99e..962e3a44 100644 --- a/tools/checks/gen1_maps.gd +++ b/tools/checks/gen1_maps.gd @@ -115,9 +115,9 @@ const PALETTE_CENSUS: Dictionary = { ## byte that opens it. Yellow's six bare `text_end`s are Jessie and James, whose ## two ids share one on three maps. const TEXT_CENSUS: Dictionary = { - &"red": {0x08: 638, 0x17: 528, 0xFF: 12, 0xF6: 12, 0xFE: 14, 0xF5: 3, 0xF7: 3}, - &"blue": {0x08: 638, 0x17: 528, 0xFF: 12, 0xF6: 12, 0xFE: 14, 0xF5: 3, 0xF7: 3}, - &"yellow": {0x08: 691, 0x17: 512, 0xFF: 12, 0xF6: 12, 0xFE: 14, 0x50: 6, 0xF5: 3, 0xF7: 3}, + &"red": {0x08: 642, 0x17: 528, 0xFF: 12, 0xF6: 12, 0xFE: 14, 0xF5: 3, 0xF7: 3}, + &"blue": {0x08: 642, 0x17: 528, 0xFF: 12, 0xF6: 12, 0xFE: 14, 0xF5: 3, 0xF7: 3}, + &"yellow": {0x08: 695, 0x17: 512, 0xFF: 12, 0xF6: 12, 0xFE: 14, 0x50: 6, 0xF5: 3, 0xF7: 3}, } ## Rows of `script_mart` across the corpus. Yellow's Celadon 5F clerk sells one @@ -129,54 +129,52 @@ const EMPTY_TEXTS: Dictionary = {&"red": 1, &"blue": 1, &"yellow": 1} ## The `text_asm` rows read as a script, and the nodes under them. const SCRIPT_CENSUS: Dictionary = { - &"red": {"rows": 318, "text": 609, "branch": 158, "choice": 41, "flag": 246, - "give_item": 44, "has_item": 20, "take_item": 11, "unknown": 0, "pokedex": 13, - "give_pokemon": 33, "saved_coord_index": 1, "badges_byte": 1, "walk": 20, - "player_facing": 11, "set_map_script": 113, "npc_movement_script": 2, - "toggle_object": 49, "trainer_battle_object": 21, "random": 5, "facing": 12, - "player_in_array": 1, "flag_test": 3, "pick_up_item": 105, "scratch": 37, - "name_badge": 7, "heal_party": 2, "object_facing": 9, "talking_to": 32, - "set_starter": 9, "name_species": 14, "dex_rating": 2, "dex_count": 2, - "map_text": 24, "trade": 9, "name_item": 7, "oaks_aide": 3, "player_coord": 4, - "money_box": 6, "has_money": 4, "spend_money": 4, "menu": 3, "menu_cancel": 3, - "menu_row": 1, "guard_drink": 4, "day_care": 1, "random_bit": 2, "volatile": 2, - "filtered_bag": 2, "menu_item": 4, "elevator": 3, "coin_box": 2, "has_coins": 4, - "add_coins": 4, "replace_block": 1, "starter": 2, "trainer_battle": 3, - "safari_balls": 1, "safari_steps": 1, "save_coord_index": 2, "map_load_bit": 2, - "copy_name": 4, "set_fossil": 6, "party_menu": 1, "name_party_mon": 1, "mon_ot": 1, - "name_mon": 1, "list_menu": 1, "emote": 1, "diploma": 1}, - &"blue": {"rows": 318, "text": 609, "branch": 158, "choice": 41, "flag": 246, - "give_item": 44, "has_item": 20, "take_item": 11, "unknown": 0, "pokedex": 13, - "give_pokemon": 33, "saved_coord_index": 1, "badges_byte": 1, "walk": 20, - "player_facing": 11, "set_map_script": 113, "npc_movement_script": 2, - "toggle_object": 49, "trainer_battle_object": 21, "random": 5, "facing": 12, - "player_in_array": 1, "flag_test": 3, "pick_up_item": 105, "scratch": 37, - "name_badge": 7, "heal_party": 2, "object_facing": 9, "talking_to": 32, - "set_starter": 9, "name_species": 14, "dex_rating": 2, "dex_count": 2, - "map_text": 24, "trade": 9, "name_item": 7, "oaks_aide": 3, "player_coord": 4, - "money_box": 6, "has_money": 4, "spend_money": 4, "menu": 3, "menu_cancel": 3, - "menu_row": 1, "guard_drink": 4, "day_care": 1, "random_bit": 2, "volatile": 2, - "filtered_bag": 2, "menu_item": 4, "elevator": 3, "coin_box": 2, "has_coins": 4, - "add_coins": 4, "replace_block": 1, "starter": 2, "trainer_battle": 3, - "safari_balls": 1, "safari_steps": 1, "save_coord_index": 2, "map_load_bit": 2, - "copy_name": 4, "set_fossil": 6, "party_menu": 1, "name_party_mon": 1, "mon_ot": 1, - "name_mon": 1, "list_menu": 1, "emote": 1, "diploma": 1}, - &"yellow": {"rows": 373, "text": 610, "branch": 160, "choice": 34, "flag": 222, - "give_item": 41, "has_item": 15, "take_item": 9, "unknown": 4, "pokedex": 10, - "give_pokemon": 8, "saved_coord_index": 2, "badges_byte": 1, "walk": 23, - "set_map_script": 99, "npc_movement_script": 2, "toggle_object": 22, + &"red": {"rows": 322, "text": 621, "branch": 162, "choice": 45, "flag": 254, "give_item": 44, + "has_item": 20, "take_item": 11, "unknown": 0, "pokedex": 13, "give_pokemon": 33, + "saved_coord_index": 1, "emote": 1, "player_facing": 11, "badges_byte": 1, "walk": 20, + "set_map_script": 113, "npc_movement_script": 2, "toggle_object": 49, + "trainer_battle_object": 21, "random": 5, "facing": 12, "player_in_array": 1, + "flag_test": 7, "pick_up_item": 105, "scratch": 37, "name_badge": 7, "heal_party": 2, + "object_facing": 9, "talking_to": 32, "set_starter": 9, "name_species": 14, + "dex_rating": 2, "dex_count": 2, "map_text": 24, "trade": 9, "name_item": 7, + "oaks_aide": 3, "player_coord": 4, "money_box": 6, "has_money": 4, "spend_money": 4, + "menu": 3, "menu_cancel": 3, "menu_row": 1, "guard_drink": 4, "day_care": 1, + "random_bit": 2, "volatile": 2, "filtered_bag": 2, "menu_item": 4, "elevator": 3, + "diploma": 1, "coin_box": 2, "has_coins": 4, "add_coins": 4, "replace_block": 1, + "starter": 2, "trainer_battle": 3, "safari_balls": 1, "safari_steps": 1, + "save_coord_index": 2, "map_load_bit": 6, "copy_name": 4, "set_fossil": 6, + "party_menu": 1, "name_party_mon": 1, "mon_ot": 1, "name_mon": 1, "list_menu": 1}, + &"blue": {"rows": 322, "text": 621, "branch": 162, "choice": 45, "flag": 254, "give_item": 44, + "has_item": 20, "take_item": 11, "unknown": 0, "pokedex": 13, "give_pokemon": 33, + "saved_coord_index": 1, "emote": 1, "player_facing": 11, "badges_byte": 1, "walk": 20, + "set_map_script": 113, "npc_movement_script": 2, "toggle_object": 49, + "trainer_battle_object": 21, "random": 5, "facing": 12, "player_in_array": 1, + "flag_test": 7, "pick_up_item": 105, "scratch": 37, "name_badge": 7, "heal_party": 2, + "object_facing": 9, "talking_to": 32, "set_starter": 9, "name_species": 14, + "dex_rating": 2, "dex_count": 2, "map_text": 24, "trade": 9, "name_item": 7, + "oaks_aide": 3, "player_coord": 4, "money_box": 6, "has_money": 4, "spend_money": 4, + "menu": 3, "menu_cancel": 3, "menu_row": 1, "guard_drink": 4, "day_care": 1, + "random_bit": 2, "volatile": 2, "filtered_bag": 2, "menu_item": 4, "elevator": 3, + "diploma": 1, "coin_box": 2, "has_coins": 4, "add_coins": 4, "replace_block": 1, + "starter": 2, "trainer_battle": 3, "safari_balls": 1, "safari_steps": 1, + "save_coord_index": 2, "map_load_bit": 6, "copy_name": 4, "set_fossil": 6, + "party_menu": 1, "name_party_mon": 1, "mon_ot": 1, "name_mon": 1, "list_menu": 1}, + &"yellow": {"rows": 378, "text": 625, "branch": 164, "choice": 38, "flag": 232, "give_item": 41, + "has_item": 15, "take_item": 10, "unknown": 3, "pokedex": 10, "give_pokemon": 9, + "saved_coord_index": 2, "player_facing": 16, "emote": 8, "badges_byte": 1, "walk": 23, + "set_map_script": 100, "npc_movement_script": 2, "toggle_object": 22, "trainer_battle_object": 27, "random": 5, "facing": 12, "player_in_array": 1, - "name_species": 7, "flag_test": 6, "pick_up_item": 109, "scratch": 29, - "name_badge": 7, "player_facing": 16, "heal_party": 2, "emote": 8, "dex_rating": 2, - "dex_count": 6, "map_text": 24, "trade": 7, "name_item": 7, "oaks_aide": 3, - "player_coord": 4, "money_box": 6, "has_money": 5, "spend_money": 4, "menu": 3, - "menu_cancel": 3, "menu_row": 1, "guard_drink": 4, "day_care": 1, "random_bit": 2, - "volatile": 2, "filtered_bag": 2, "menu_item": 4, "elevator": 3, "coin_box": 2, + "name_species": 8, "flag_test": 10, "pick_up_item": 109, "scratch": 29, + "name_badge": 7, "heal_party": 2, "dex_rating": 2, "dex_count": 6, "set_starter": 1, + "map_text": 24, "trade": 7, "name_item": 7, "oaks_aide": 3, "player_coord": 4, + "money_box": 6, "has_money": 5, "spend_money": 4, "menu": 3, "menu_cancel": 3, + "menu_row": 1, "guard_drink": 4, "day_care": 1, "random_bit": 2, "volatile": 2, + "filtered_bag": 2, "menu_item": 4, "elevator": 3, "diploma": 1, "coin_box": 2, "has_coins": 4, "add_coins": 4, "replace_block": 1, "trainer_battle": 1, "safari_balls": 3, "safari_steps": 3, "safari_admission": 2, "save_coord_index": 2, - "map_load_bit": 2, "talking_to": 14, "volatile_test": 6, "copy_name": 4, + "map_load_bit": 6, "talking_to": 14, "volatile_test": 6, "copy_name": 4, "set_fossil": 6, "party_menu": 1, "name_party_mon": 1, "mon_ot": 1, "name_mon": 1, - "list_menu": 1, "diploma": 1}, + "list_menu": 1}, } ## `SilphCo11FPorygonText` is a `call DisplayPokedex` the disassembly marks ## unreferenced. The `trade` rows are the eight `predef DoInGameTradeDialogue` @@ -226,7 +224,7 @@ const CALLBACK_CENSUS: Dictionary = { ## every `set_map_script` already read, and the bodies the walker gets whole. const STATE_CENSUS: Dictionary = { &"red": {"tables": 98, "states": 373, "read": 183, "branch": 71, "player_coord": 53, - "player_facing": 58, "flag": 264, "set_map_script": 313, "save_coord_index": 9, + "player_facing": 58, "flag": 264, "set_map_script": 318, "save_coord_index": 9, "map_text": 142, "toggle_object": 155, "object_facing": 75, "set_player_coord": 1, "object_path": 2, "movement_running": 70, "npc_movement_script": 1, "movement_script_running": 4, "flag_test": 15, "walk": 47, "badges_byte": 1, @@ -234,14 +232,14 @@ const STATE_CENSUS: Dictionary = { "riding": 4, "coord_index": 18, "starter": 22, "trainer_battle": 30, "battle_outcome": 34, "object_stay": 12, "facing": 3, "has_item": 4, "emote": 2, "saved_coord_index": 16, "badge_guards": 1, "scratch_test": 4, "set_starter": 3, - "name_species": 3, "heal_party": 2, "scratch": 13, "give_item": 9, - "arrow_movement": 3, "guard_drink": 4, "boulder_on": 3, "map_load_bit": 10, - "hall_of_fame": 1, "set_blackout_map": 1, "save_game": 1, "reset_game": 1, - "object_coord_move": 1, "warp_to": 1, "set_last_map": 1, "safari_balls": 1, - "set_riding": 1, "replace_block": 24, "volatile": 1, "volatile_test": 1, - "coord_lookup": 3, "trainer_battle_object": 1}, + "name_species": 3, "heal_party": 2, "scratch": 13, "give_item": 9, "arrow_movement": 3, + "guard_drink": 4, "boulder_on": 3, "map_load_bit": 10, "hall_of_fame": 1, + "set_blackout_map": 1, "save_game": 1, "reset_game": 1, "object_coord_move": 1, + "warp_to": 1, "set_last_map": 1, "safari_balls": 1, "set_riding": 1, + "replace_block": 24, "volatile": 1, "volatile_test": 1, "coord_lookup": 3, + "trainer_battle_object": 1}, &"blue": {"tables": 98, "states": 373, "read": 183, "branch": 71, "player_coord": 53, - "player_facing": 58, "flag": 264, "set_map_script": 313, "save_coord_index": 9, + "player_facing": 58, "flag": 264, "set_map_script": 318, "save_coord_index": 9, "map_text": 142, "toggle_object": 155, "object_facing": 75, "set_player_coord": 1, "object_path": 2, "movement_running": 70, "npc_movement_script": 1, "movement_script_running": 4, "flag_test": 15, "walk": 47, "badges_byte": 1, @@ -249,27 +247,26 @@ const STATE_CENSUS: Dictionary = { "riding": 4, "coord_index": 18, "starter": 22, "trainer_battle": 30, "battle_outcome": 34, "object_stay": 12, "facing": 3, "has_item": 4, "emote": 2, "saved_coord_index": 16, "badge_guards": 1, "scratch_test": 4, "set_starter": 3, - "name_species": 3, "heal_party": 2, "scratch": 13, "give_item": 9, - "arrow_movement": 3, "guard_drink": 4, "boulder_on": 3, "map_load_bit": 10, - "hall_of_fame": 1, "set_blackout_map": 1, "save_game": 1, "reset_game": 1, - "object_coord_move": 1, "warp_to": 1, "set_last_map": 1, "safari_balls": 1, - "set_riding": 1, "replace_block": 24, "volatile": 1, "volatile_test": 1, - "coord_lookup": 3, "trainer_battle_object": 1}, - &"yellow": {"tables": 98, "states": 408, "read": 223, "branch": 123, "player_coord": 79, - "flag": 301, "player_facing": 63, "set_map_script": 369, "save_coord_index": 15, - "map_text": 174, "object_position": 12, "toggle_object": 181, "object_facing": 85, - "set_player_coord": 1, "object_path": 2, "movement_running": 81, "wild_battle": 6, - "npc_movement_script": 1, "movement_script_running": 4, "flag_test": 20, - "walk": 61, "badges_byte": 6, "object_move": 93, "player_in_array": 38, - "riding": 4, "coord_index": 18, "trainer_battle": 17, "battle_outcome": 38, - "object_stay": 17, "facing": 4, "has_item": 4, "emote": 2, "saved_coord_index": 18, - "starter": 1, "set_starter": 4, "badge_guards": 1, "name_species": 1, - "heal_party": 3, "give_item": 9, "arrow_movement": 3, "guard_drink": 4, - "volatile_test": 3, "volatile": 8, "random": 1, "boulder_on": 3, - "map_load_bit": 10, "hall_of_fame": 1, "set_blackout_map": 1, "save_game": 1, - "reset_game": 1, "warp_to": 1, "set_last_map": 1, "safari_balls": 1, - "set_riding": 1, "scratch_test": 3, "scratch": 8, "replace_block": 36, - "coord_lookup": 3, "trainer_battle_object": 2}, + "name_species": 3, "heal_party": 2, "scratch": 13, "give_item": 9, "arrow_movement": 3, + "guard_drink": 4, "boulder_on": 3, "map_load_bit": 10, "hall_of_fame": 1, + "set_blackout_map": 1, "save_game": 1, "reset_game": 1, "object_coord_move": 1, + "warp_to": 1, "set_last_map": 1, "safari_balls": 1, "set_riding": 1, + "replace_block": 24, "volatile": 1, "volatile_test": 1, "coord_lookup": 3, + "trainer_battle_object": 1}, + &"yellow": {"tables": 98, "states": 411, "read": 226, "branch": 123, "player_coord": 86, + "flag": 306, "player_facing": 63, "set_map_script": 384, "save_coord_index": 15, + "map_text": 181, "object_position": 28, "toggle_object": 195, "object_facing": 94, + "set_player_coord": 1, "object_path": 2, "movement_running": 83, "wild_battle": 6, + "npc_movement_script": 1, "movement_script_running": 4, "flag_test": 20, "walk": 61, + "badges_byte": 6, "object_move": 102, "player_in_array": 38, "riding": 4, + "coord_index": 18, "trainer_battle": 17, "battle_outcome": 38, "object_stay": 17, + "facing": 4, "has_item": 4, "emote": 2, "saved_coord_index": 18, "starter": 1, + "set_starter": 4, "badge_guards": 1, "name_species": 1, "heal_party": 3, "scratch": 16, + "give_item": 9, "arrow_movement": 3, "guard_drink": 4, "volatile_test": 3, + "volatile": 8, "random": 1, "boulder_on": 3, "map_load_bit": 10, "hall_of_fame": 1, + "set_blackout_map": 1, "save_game": 1, "reset_game": 1, "warp_to": 1, + "set_last_map": 1, "safari_balls": 1, "set_riding": 1, "scratch_test": 3, + "replace_block": 36, "coord_lookup": 3, "trainer_battle_object": 2}, } ## The pin on which way a `wCurrentMenuItem` branch reads. @@ -715,6 +712,9 @@ func _walk_script( if op == "text" and not _drawn(font, String(node["text"]).replace("\n", "")) \ and wrong.size() < 4: wrong.append("map %d draws a blank tile" % number) + if op == "text" and String(node["text"]).contains(Gen2TextStream.NUMBER_MARKER) \ + and wrong.size() < 4: + wrong.append("map %d prints a number nothing wrote" % number) if node.has("flag") and op in ["flag", "branch", "flag_test"]: var flag: int = int(node["flag"]) if (flag < 0 or flag >= Gen1Layout.EVENT_FLAG_BYTES * 8) and wrong.size() < 4: @@ -803,25 +803,55 @@ func _hidden_events() -> void: _r.note("gen1 hidden events %s, %d bookshelf tiles" % [census, bookshelves]) +## Every box under [param rows]' scripts, branches included. +func _script_texts(rows: Array) -> Array[String]: + var out: Array[String] = [] + var pending: Array = [] + for row: Dictionary in rows: + pending.append(row.get("script", [])) + while not pending.is_empty(): + for node: Dictionary in pending.pop_back() as Array: + if String(node["op"]) == "text": + out.append(String(node["text"])) + for side: String in Gen1Layout.SCRIPT_BRANCH_KEYS: + if node.has(side): + pending.append(node[side]) + return out + + func _walk_hidden( nodes: Array, census: Dictionary, wrong: Array[String], number: int ) -> void: for node: Dictionary in nodes: var op: String = String(node["op"]) census[op] = int(census.get(op, 0)) + 1 - if op == "facing" and not Gen1Layout.FACING_STEPS.has(int(node["facing"])) \ - and wrong.size() < 4: - wrong.append("map %d faces %d" % [number, int(node["facing"])]) - if op == "text" and String(node["text"]).is_empty() and wrong.size() < 4: - wrong.append("map %d prints nothing" % number) - if (op == "give_item" or op == "has_item" or op == "name_item") \ - and _r.data.item_name(int(node["item"])).is_empty(): - wrong.append("map %d names item %d" % [number, int(node["item"])]) + var fault: String = _hidden_node_fault(node, number) + if not fault.is_empty() and wrong.size() < 4: + wrong.append("map %d %s" % [number, fault]) for side: String in Gen1Layout.SCRIPT_BRANCH_KEYS: if node.has(side): _walk_hidden(node[side] as Array, census, wrong, number) +## What is wrong with one hidden event node, or "". +func _hidden_node_fault(node: Dictionary, number: int) -> String: + var op: String = String(node["op"]) + if op == "facing" and not Gen1Layout.FACING_STEPS.has(int(node["facing"])): + return "faces %d" % int(node["facing"]) + if op == "text" and String(node["text"]).is_empty(): + return "prints nothing" + if op == "text" and String(node["text"]).contains(Gen2TextStream.NUMBER_MARKER): + return "prints a number nothing wrote" + ## The Mansion's switches name rows past every object's, read all the same. + if op == "map_text" and node.has("text") \ + and (_maps[number] as Gen2WorldMap).text_at(int(node["text"])).is_empty(): + return "opens text %d, which was not read" % int(node["text"]) + if op in ["give_item", "has_item", "name_item"] \ + and _r.data.item_name(int(node["item"])).is_empty(): + return "names item %d" % int(node["item"]) + return "" + + ## `LavenderTownLittleGirlText`: the question, then `wCurrentMenuItem` zero for ## YES. Its `ld hl` between `and a` and the `jr nz` is why a branch is decoded ## off the flags rather than off the instruction in front of it. @@ -1232,6 +1262,7 @@ const SAFARI_TEXTS: int = 6 const SAFARI_BATTLE_MAPS: int = 4 const SAFARI_WINDOW_MAPS: int = 9 const SAFARI_STEPS_LABEL: String = "/500" +const SAFARI_FEE_LINE: String = "That'll be ¥500" const SAFARI_MENU_TOP: String = "BALL×" @@ -1243,6 +1274,10 @@ func _safari() -> void: "the gate reads %d states." % (gate.scripts["states"] as Array).size()) _r.check(gate.texts.size() == SAFARI_TEXTS, "the gate reads %d text rows." % gate.texts.size()) + ## Yellow's `text_bcd wPriceTemp` where Red spells the fee out. + _r.check(_script_texts(gate.texts).any( + func(text: String) -> bool: return text.contains(SAFARI_FEE_LINE)), + "no gate text says %s." % [SAFARI_FEE_LINE]) var battle_maps: int = 0 var window_maps: int = 0 for number: int in _maps: diff --git a/tools/checks/gen1_pics.gd b/tools/checks/gen1_pics.gd index 9213eba0..5e22858d 100644 --- a/tools/checks/gen1_pics.gd +++ b/tools/checks/gen1_pics.gd @@ -18,7 +18,7 @@ const TRAINER_SIDE: int = 7 ## SHA-1 of each atlas's own index buffer. Red and Blue share every picture, ## and Yellow redrew all 151 front pics, six trainer ones (Brock, Misty, Erika -## and the rival's three) and `RedPicFront`, but no back pic at all. +## and the rival's three) and `RedPicFront`, and added `ProfOakPicBack` alone. const DIGESTS: Dictionary = { &"red": { "front": "672ba02e2d98a787fb619e11e77fba45a379a47b", @@ -38,7 +38,7 @@ const DIGESTS: Dictionary = { "front": "d6e50eed9888dbe7787b1a1952403eab5bd133b6", "back": "a8976278a8c8294f285a25a993a44f2f6012b127", "trainers": "6b0fe80efffb9a8223a9646b1df42cfa592fd0f4", - "player_back": "6c2830e479cf877b20bd10f6c94324bb5c3e425d", + "player_back": "161795669025786e3c4d004a400325e05b53d57b", "player_front": "070dcf72706fc8ab991158a8bd32beae0c7a0d36", }, } @@ -106,7 +106,7 @@ func _one_game() -> void: _atlas("front", SPECIES_COUNT, FRONT_SIDES[FRONT_SIDES.size() - 1]) _atlas("back", SPECIES_COUNT, BACK_SIDE) _atlas("trainers", TRAINER_COUNT, TRAINER_SIDE) - _atlas("player_back", Gen1Layout.PLAYER_BACKPICS.size(), BACK_SIDE) + _atlas("player_back", _backpics().size(), BACK_SIDE) _atlas("player_front", PLAYER_FRONTPICS, TRAINER_SIDE) _species_pics() _trainer_pics() @@ -181,13 +181,23 @@ func _trainer_pics() -> void: ) +func _backpics() -> Array[String]: + return Gen1Layout.player_backpics(Gen1Layout.for_id(_r.game_id)) + + func _player_pics() -> void: + var backpics: Array[String] = _backpics() + _r.check( + backpics.size() == (3 if _r.game_id == RomRegistry.YELLOW else 2), + "the cartridge draws %d back pics." % backpics.size() + ) var drawn: Array[PackedByteArray] = [] - for slot: int in Gen1Layout.PLAYER_BACKPICS.size(): + for slot: int in backpics.size(): var cell: Dictionary = _cell("player_back", slot) - _r.check(_ink(cell), "the %s back pic is blank." % Gen1Layout.PLAYER_BACKPICS[slot]) - drawn.append(cell.get("indices", PackedByteArray())) - _r.check(drawn[0] != drawn[1], "the player and the old man share a back pic.") + _r.check(_ink(cell), "the %s back pic is blank." % backpics[slot]) + var indices: PackedByteArray = cell.get("indices", PackedByteArray()) + _r.check(not drawn.has(indices), "the %s back pic repeats another." % backpics[slot]) + drawn.append(indices) var front: Dictionary = _cell("player_front", 0) _r.check(_ink(front), "RedPicFront is blank.") _r.check( @@ -245,7 +255,7 @@ func _battle_page() -> void: _r.check(drawn != blank, "battle tile $%02X is blank." % tile) var cropped: Array[int] = [] - for slot: int in SPECIES_COUNT + Gen1Layout.PLAYER_BACKPICS.size(): + for slot: int in SPECIES_COUNT + _backpics().size(): var name: String = "back" if slot < SPECIES_COUNT else "player_back" if _draws_outside_crop(name, slot if slot < SPECIES_COUNT else slot - SPECIES_COUNT): cropped.append(slot + 1) diff --git a/tools/checks/gen1_trainers.gd b/tools/checks/gen1_trainers.gd index 485dd808..a42717ad 100644 --- a/tools/checks/gen1_trainers.gd +++ b/tools/checks/gen1_trainers.gd @@ -15,9 +15,16 @@ const CLASS_COUNT: int = 47 ## `text_asm` rows, the ones reaching `TalkToTrainer`, and the objects naming ## one: a trainer class above `OPP_ID_OFFSET`, or a standing wild below it. const HEADER_CENSUS: Dictionary = { - &"red": {"text_asm": 638, "headers": 322, "trainers": 310, "wilds": 12}, - &"blue": {"text_asm": 638, "headers": 322, "trainers": 310, "wilds": 12}, - &"yellow": {"text_asm": 691, "headers": 317, "trainers": 305, "wilds": 12}, + &"red": {"text_asm": 642, "headers": 322, "trainers": 310, "wilds": 12, "coded": 3}, + &"blue": {"text_asm": 642, "headers": 322, "trainers": 310, "wilds": 12, "coded": 3}, + &"yellow": {"text_asm": 695, "headers": 317, "trainers": 305, "wilds": 12, "coded": 3}, +} +## The header texts whose machine code does more than print, by map and the +## flag each sets; Yellow moved the LIFT KEY's `ShowObject` to the end text. +const CODED_HEADER_FLAGS: Dictionary = { + &"red": {"113:after": 2302, "199:end": 1653, "202:after": 1702}, + &"blue": {"113:after": 2302, "199:end": 1653, "202:after": 1702}, + &"yellow": {"113:after": 2302, "199:end": 1653, "202:end": 1702}, } ## `view_range << 4` is a pixel distance, so the stored range is a nibble. @@ -175,7 +182,8 @@ func _the_party_table() -> void: ## Every header the corpus carries, and the object each belongs to: three texts, ## a range inside its nibble, a flag above zero, and a party that is stored. func _the_headers() -> void: - var census: Dictionary = {"text_asm": 0, "headers": 0, "trainers": 0, "wilds": 0} + var census: Dictionary = {"text_asm": 0, "headers": 0, "trainers": 0, "wilds": 0, "coded": 0} + var coded: Dictionary = {} for map: Gen2WorldMap in _r.data.world_maps(): for row: Dictionary in map.texts: if int(row.get("command", 0)) == Gen1Layout.TEXT_ASM: @@ -183,6 +191,12 @@ func _the_headers() -> void: if row.has("trainer"): census["headers"] += 1 _one_header(map, row["trainer"]) + for name: String in ["before", "after", "end"]: + var flag: int = _first_flag((row["trainer"] as Dictionary).get("%s_script" % name, [])) + if flag < 0: + continue + census["coded"] += 1 + coded["%d:%s" % [map.number, name]] = flag for object: Dictionary in map.events["objects"] as Array: var header: Dictionary = _header_for(map, object) if header.is_empty(): @@ -206,11 +220,19 @@ func _the_headers() -> void: "map %d's standing wild is outside ToggleableObjectStates." % map.number) _r.check(census == HEADER_CENSUS[_r.game_id], "the header census reads %s." % str(census)) + _r.check(coded == CODED_HEADER_FLAGS[_r.game_id], "the coded header texts set %s." % [coded]) _r.note("gen1 trainers %d headers on %d text_asm rows" % [ int(census["headers"]), int(census["text_asm"]), ]) +func _first_flag(script: Array) -> int: + for node: Dictionary in script: + if String(node.get("op", "")) == "flag": + return int(node["flag"]) + return -1 + + func _one_header(map: Gen2WorldMap, header: Dictionary) -> void: _r.check(int(header["event_flag"]) > 0, "map %d has a header on flag %d." % [map.number, int(header["event_flag"])]) @@ -275,7 +297,7 @@ func _talk_to( var where: String = "map %d text %d" % [map.number, int(object.get("text", 0))] if not _r.check(not opened.is_empty(), "%s said nothing." % where): return false - if not _r.check(_event_text(opened) == String(header["before"]), + if not _r.check(_event_text(opened) == world.gen1_filled_text(String(header["before"])), "%s opened with %s." % [where, _event_text(opened)]): return false var request: Dictionary = _request_after(world) @@ -290,8 +312,10 @@ func _talk_to( if not object.has("trainer_class"): return _r.check(_face(world, object).is_empty(), "%s is still on the map." % where) var again: Array = _face(world, object) - var answered: bool = _r.check(_event_text(again) == String(header["after"]), - "%s finished with %s." % [where, _event_text(again)]) + var answered: bool = _r.check( + _event_text(again) == world.gen1_filled_text(String(header["after"])), + "%s finished with %s." % [where, _event_text(again)] + ) world.run_event_queue(true) return answered diff --git a/tools/checks/gen1_walk.gd b/tools/checks/gen1_walk.gd index db02829a..973b78f2 100644 --- a/tools/checks/gen1_walk.gd +++ b/tools/checks/gen1_walk.gd @@ -495,6 +495,8 @@ func _one_game() -> void: _check_the_saffron_guard() _check_an_arrow_tile() _check_a_scripted_wild_battle() + _check_the_ghost_marowak() + _check_the_catch_training() if _r.game_id != RomRegistry.YELLOW: _check_the_opening_walk() _check_the_route_23_guards() @@ -515,6 +517,10 @@ func _one_game() -> void: _check_the_captains_back() _check_the_ship_leaves() _check_the_gate_pushes_back() + _check_cinnabar_settles() + _check_a_mansion_switch() + _check_an_elite_room_settles() + _check_lances_trigger() ## `DisplayPokemonCenterDialogue_` walked whole. `AnimateHealingMachine` is a @@ -776,17 +782,22 @@ func _check_a_card_key_door() -> void: "the door opened at %s was remembered as %s." % [ SILPH_DOOR, world.state.card_key_door(), ]) + ## `set BIT_CUR_MAP_LOADED_1`: the next frame's callback turns the + ## coordinates into the door's flag before the player takes a step. + world.dispatch_sight_events() + var flag: int = int((world.current_map.events["card_key"] as Array)[0]["flag"]) + _r.check(world.state.card_key_door() == Gen2WorldState.NO_CARD_KEY_DOOR + and world.event_flag_active(flag), + "the frame after the box kept %s and flag %d %s." % [ + world.state.card_key_door(), flag, world.event_flag_active(flag)]) - ## The floor loaded again: the coordinates become the door's flag and the - ## callback leaves that one alone. + ## The floor loaded again: the callback leaves the flagged door alone. var again: Gen2WorldAPI = _r.open_world( 0, SILPH_CO_2F, SILPH_DOOR_APPROACH, world.state ) if again == null: return again.dispatch_map_entry() - _r.check(again.state.card_key_door() == Gen2WorldState.NO_CARD_KEY_DOOR, - "the reloaded floor kept %s." % [again.state.card_key_door()]) _r.check(again.block_at(SILPH_DOOR.x, SILPH_DOOR.y) == SILPH_OPEN_BLOCK, "the reloaded floor blocked the opened door with $%02X." % again.block_at( SILPH_DOOR.x, SILPH_DOOR.y @@ -2614,6 +2625,89 @@ func _check_a_scripted_player_walk() -> void: _r.check(world.player_facing == Gen2WorldSprite.FACING_RIGHT, "the player faced %d rather than Oak." % world.player_facing) _r.note("gen1 walk HALL_OF_FAME five cells in and Oak turning to meet it") + _check_the_induction(world) + + +## `HallOfFameResetEventsAndSaveScript` behind Oak's box: `HallOfFamePC` is a +## request the screen answers with the induction, and the flag the shelf reads +## stands before it; the Plateau's events, the save and `jp Init` follow. +func _check_the_induction(world: Gen2WorldAPI) -> void: + world.state.set_event_flag(BEAT_CHAMPION_RIVAL_FLAG) + var request: Dictionary = _pressed_to_request(world) + if not _r.check(StringName(request.get("kind", &"")) == &"hall_of_fame_requested", + "Oak's box was followed by %s." % [request]): + return + _r.check(world.state.hall_of_fame(), "ENGINE_HALL_OF_FAME is clear at the induction.") + world.complete_runtime_request({"ok": true}) + var kinds: Array = [] + for _request: int in 3: + var next: Dictionary = _pressed_to_request(world) + if next.is_empty(): + break + kinds.append(StringName(next["kind"])) + if StringName(next["kind"]) == &"soft_reset_requested": + break + world.complete_runtime_request({"ok": true, "script_value": 1}) + _r.check(kinds == [&"quick_save_requested", &"soft_reset_requested"], + "the induction was followed by %s." % [kinds]) + _r.check(not world.event_flag_active(BEAT_CHAMPION_RIVAL_FLAG), + "the Plateau's events were not cleared.") + + +## A pressed through every box until a runtime request stands, or {}. +func _pressed_to_request(world: Gen2WorldAPI) -> Dictionary: + for _pass: int in SCRIPTED_WALK_PASSES: + if not world.pending_runtime_request().is_empty(): + return world.pending_runtime_request() + if world.pending_script_input().is_empty(): + world.dispatch_sight_events() + else: + world.run_event_queue(true) + return world.pending_runtime_request() + + +## `LoreleisRoomLoreleiEndBattleScript` calls `EndTrainerBattle`, whose +## `ResetButtonPressedAndMapScript` zeroes `wCurMapScript`, so the after-battle +## line prints once and the room settles on its default state. +func _check_an_elite_room_settles() -> void: + var world: Gen2WorldAPI = _r.open_world(0, LORELEIS_ROOM, LORELEI_SIDE) + if world == null: + return + world.state.set_event_flag(AUTOWALKED_INTO_LORELEIS_ROOM_FLAG) + world.dispatch_map_entry() + world.player_facing = Gen2WorldSprite.FACING_UP + world.interact() + var request: Dictionary = _pressed_to_request(world) + if not _r.check(StringName(request.get("kind", &"")) == &"battle_requested", + "Lorelei asked for %s." % [request]): + return + world.complete_runtime_request({"ok": true, "outcome": Gen2WorldBattleAdapter.OUTCOME_WON}) + var boxes: int = 0 + for _pass: int in 6: + if not world.pending_script_input().is_empty(): + boxes += 1 + world.run_event_queue(true) + else: + world.dispatch_sight_events() + _r.check(world.state.gen1_map_script(LORELEIS_ROOM_BYTE) == 0, + "the room stayed on state %d." % world.state.gen1_map_script(LORELEIS_ROOM_BYTE)) + _r.check(boxes == 1, "the after-battle line printed %d times." % boxes) + _r.check(world.event_flag_active(BEAT_LORELEI_FLAG), "EVENT_BEAT_LORELEIS_ROOM_TRAINER_0 is clear.") + _r.note("gen1 walk LORELEIS_ROOM's end-battle state hands back to the default one") + + +## `DisplayTextID` by id: `hTextID` is `hSpriteIndex`, so the object of that +## index is the trainer `TalkToTrainer` fights, which is how Lance's own +## coordinate trigger opens a trainer battle rather than a wild one. +func _check_lances_trigger() -> void: + var world: Gen2WorldAPI = _r.open_world(0, LANCES_ROOM, LANCE_TRIGGER) + if world == null: + return + world.dispatch_map_entry() + var values: Dictionary = _pressed_to_request(world).get("values", {}) as Dictionary + _r.check(StringName(values.get("kind", &"")) == &"trainer" + and int(values.get("trainer_class", 0)) == LANCE_CLASS, + "Lance's trigger asked for %s." % [values]) ## `RemoveGuardDrink` driven on the world: the guard is thirsty with an empty @@ -2708,6 +2802,84 @@ func _check_a_scripted_wild_battle() -> void: _r.note("gen1 walk ROUTE_12's Snorlax fought at level %d" % SNORLAX_LEVEL) +## `PokemonTower6FMarowakBattleScript` reads `wBattleResult` with `and a`: only +## a won fight sets EVENT_BEAT_GHOST_MAROWAK and prints the departure. +const POKEMON_TOWER_6F: int = 147 +const POKEMON_TOWER_6F_BYTE: int = 63 +const MAROWAK_CELL := Vector2i(10, 16) +const MAROWAK_SPECIES: int = 105 +const MAROWAK_LEVEL: int = 30 +const MAROWAK_BEAT_FLAG: int = 271 +const MAROWAK_DEPARTED: String = "The GHOST was" + + +func _check_the_ghost_marowak() -> void: + for outcome: StringName in [Gen2WorldBattleAdapter.OUTCOME_RAN, Gen2WorldBattleAdapter.OUTCOME_WON]: + var world: Gen2WorldAPI = _r.open_world(0, POKEMON_TOWER_6F, MAROWAK_CELL + Vector2i.RIGHT) + if world == null: + return + world.player_cell = MAROWAK_CELL + var opened: Array = world.dispatch_sight_events() + var passes: int = 0 + while world.pending_runtime_request().is_empty() and passes < SCRIPTED_WALK_PASSES: + world.run_event_queue(true) + passes += 1 + var values: Dictionary = world.pending_runtime_request().get("values", {}) as Dictionary + if not _r.check( + int(values.get("pokemon", 0)) == MAROWAK_SPECIES and int(values.get("level", 0)) == MAROWAK_LEVEL, + "the ghost asked for %s / %s." % [world.pending_runtime_request(), opened] + ): + return + world.complete_runtime_request({"ok": true, "outcome": outcome}) + var after: Array = world.dispatch_sight_events() + var won: bool = outcome == Gen2WorldBattleAdapter.OUTCOME_WON + _r.check(world.event_flag_active(MAROWAK_BEAT_FLAG) == won, + "a fight %s left EVENT_BEAT_GHOST_MAROWAK %s." % [outcome, world.event_flag_active(MAROWAK_BEAT_FLAG)]) + _r.check((not after.is_empty() and _event_text(after).begins_with(MAROWAK_DEPARTED)) == won, + "a fight %s was answered with %s." % [outcome, after]) + _r.note("gen1 walk POKEMON_TOWER_6F's MAROWAK fought at level %d" % MAROWAK_LEVEL) + + +## `ViridianCityOldManStartCatchTrainingScript`'s `wBattleType` as the Dude's +## tutorial, and `wCurOpponent` read behind the whole state: Yellow's initial +## training sets EVENT_INITIAL_CATCH_TRAINING after the store and breaks out on it. +const VIRIDIAN_BYTE: int = 4 +## Each row: the state to start on, the one it moves to, and whether the ball lands. +const CATCH_TRAINING_STATES: Dictionary = { + &"red": [[1, 2, true]], &"blue": [[1, 2, true]], &"yellow": [[3, 4, true], [7, 8, false]], +} + + +func _check_the_catch_training() -> void: + for row: Array in CATCH_TRAINING_STATES[_r.game_id] as Array: + var world: Gen2WorldAPI = _r.open_world(0, VIRIDIAN_CITY, Vector2i(23, 10)) + if world == null: + return + world.state.set_gen1_map_script(VIRIDIAN_BYTE, int(row[0])) + var results: Array = world.dispatch_sight_events() + var request: Dictionary = world.pending_runtime_request() + var values: Dictionary = request.get("values", {}) as Dictionary + if not _r.check( + StringName(request.get("kind", &"")) == &"battle_requested" + and bool(values.get("tutorial", false)) + and int(values.get("battle_type", -1)) == Gen2Battle.BATTLETYPE_TUTORIAL + and int(values.get("gen1_battle_type", -1)) == Gen1Layout.BATTLE_TYPE_OLD_MAN, + "state %d asked for %s / %s." % [row[0], request, results] + ): + return + _r.check(world.state.gen1_map_script(VIRIDIAN_BYTE) == int(row[1]), + "the state behind the old man's battle is %d." % world.state.gen1_map_script(VIRIDIAN_BYTE)) + _r.check(world.gen1_tutorial_ball_lands() == bool(row[2]), + "the old man's ball from state %d %s." % [ + row[0], "landed" if world.gen1_tutorial_ball_lands() else "broke out"]) + world.complete_runtime_request({ + "ok": true, "outcome": Gen2WorldBattleAdapter.OUTCOME_CAUGHT, + }) + _r.check(world.pending_runtime_request().is_empty(), "the training left a request standing.") + _r.note("gen1 walk VIRIDIAN_CITY's catch training from %d states" % ( + CATCH_TRAINING_STATES[_r.game_id] as Array).size()) + + func _saffron_drink_flag() -> int: return Gen1Layout.engine_flag_base("status_flags_1") + SAFFRON_DRINK_BIT @@ -3253,6 +3425,63 @@ func _check_the_viridian_gym_door() -> void: _r.note("gen1 walk VIRIDIAN_CITY's gym door with and without seven badges") +const LORELEIS_ROOM: int = 245 +const LORELEIS_ROOM_BYTE: int = 93 +const LORELEI_SIDE := Vector2i(5, 3) +const AUTOWALKED_INTO_LORELEIS_ROOM_FLAG: int = 2278 +const BEAT_LORELEI_FLAG: int = 2273 +const LANCES_ROOM: int = 113 +const LANCE_TRIGGER := Vector2i(6, 2) +const LANCE_CLASS: int = 47 +const CINNABAR_ISLAND: int = 8 +const CINNABAR_SHORE := Vector2i(19, 13) +const POKEMON_MANSION_3F: int = 215 +const MANSION_3F_SWITCH := Vector2i(10, 6) +const MANSION_3F_DOOR := Vector2i(15, 10) +const MANSION_SWITCH_FLAG: int = 632 +const MANSION_SWITCH_ASKS: String = "A secret switch!" + + +## `CinnabarIsland_Script` sets BIT_CUR_MAP_LOADED_1 on every frame and nothing +## on the island reads it, so a walk there is not forever owed a load pass. +func _check_cinnabar_settles() -> void: + var world: Gen2WorldAPI = _r.open_world(0, CINNABAR_ISLAND, CINNABAR_SHORE) + if world == null: + return + world.dispatch_map_entry() + for _pass: int in 3: + world.dispatch_sight_events() + _r.check(not world.gen1_map_load_pending(), + "Cinnabar Island still owes a map-load pass after three frames.") + _r.check(not world.event_flag_active(MANSION_SWITCH_FLAG), + "the island did not clear EVENT_MANSION_SWITCH_ON.") + + +## `Mansion3Script_Switches`: the statue's row sits past every object's, asks, +## flips EVENT_MANSION_SWITCH_ON and sets the bit that redraws the doors. +func _check_a_mansion_switch() -> void: + var world: Gen2WorldAPI = _r.open_world(0, POKEMON_MANSION_3F, MANSION_3F_SWITCH) + if world == null: + return + world.dispatch_map_entry() + world.player_facing = Gen2WorldSprite.FACING_UP + var shut: bool = not world.can_walk_to(MANSION_3F_DOOR) + world.interact() + var asked: Dictionary = world.pending_script_input() + if not _r.check(StringName(asked.get("type", &"")) == &"choice" + and String(asked.get("text", "")).begins_with(MANSION_SWITCH_ASKS), + "the 3F switch asked %s." % [asked]): + return + world.choose_script_input(0) + world.run_event_queue(true) + world.dispatch_sight_events() + _r.check(world.event_flag_active(MANSION_SWITCH_FLAG), "the pressed switch left the event clear.") + _r.check(shut and world.can_walk_to(MANSION_3F_DOOR), + "the door at %s stood %s before and %s after." % [ + MANSION_3F_DOOR, "shut" if shut else "open", "open" if world.can_walk_to(MANSION_3F_DOOR) else "shut"]) + _r.note("gen1 walk POKEMON_MANSION_3F's switch opens its door") + + const POKEMON_TOWER_7F: int = 148 const TOWER_7F_BYTE: int = 0x40 const TOWER_7F_WARP_STATE: Dictionary = {&"red": 4, &"blue": 4, &"yellow": 11} @@ -3476,6 +3705,7 @@ func _check_the_safari_zone() -> void: "the fee left %d." % world.state.money(Gen2WorldMartHost.MONEY_ACCOUNT) ) _check_the_safari_game_ends(world.state) + _check_leaving_early() _check_the_safari_zone_refuses() _r.note("gen1 walk paid the SAFARI ZONE's %d and walked its %d steps out" % [ SAFARI_ADMISSION, Gen1Layout.SAFARI_STEPS, @@ -3514,6 +3744,44 @@ func _check_the_safari_game_ends(state: Gen2WorldState) -> void: "the gate left %d balls." % world.state.safari_balls()) +## `SafariZoneGateSafariZoneWorker1LeavingEarlyText`'s YES: the walk down, both +## events cleared, and `wNextSafariZoneGateScript`'s 0 rather than NO's 5. +const SAFARI_GATE_TOP := Vector2i(3, 0) +const SAFARI_SCRIPT_LEAVE_EARLY_LANDING := Vector2i(3, 3) + + +func _check_leaving_early() -> void: + var state := Gen2WorldState.new() + state.set_event_flag(Gen1Layout.IN_SAFARI_ZONE_EVENT, true) + state.set_safari_balls(Gen1Layout.SAFARI_BALLS) + state.set_safari_steps(Gen1Layout.SAFARI_STEPS) + var world: Gen2WorldAPI = _r.open_world(0, SAFARI_GATE, SAFARI_GATE_TOP, state) + if world == null: + return + world.state.set_gen1_map_script(world.gen1_safari_gate_byte(), SAFARI_SCRIPT_MOVING_UP) + ## `SafariZoneGatePlayerMovingUpScript` hands the question to the next frame. + world.dispatch_sight_events() + world.dispatch_sight_events() + if not _r.check( + StringName(world.pending_script_input().get("type", &"")) == &"choice", + "the gate asked %s of a player leaving early." % [world.pending_script_input()] + ): + return + _safari_answer(world, 0) + var passes: int = 0 + while (world.gen1_player_movement_running() or world.player_step_in_progress()) and passes < SCRIPTED_WALK_PASSES: + world.advance_player_step_pass() + world.dispatch_sight_events() + passes += 1 + world.dispatch_sight_events() + _r.check(world.player_cell == SAFARI_SCRIPT_LEAVE_EARLY_LANDING + and world.state.gen1_map_script(world.gen1_safari_gate_byte()) == 0, + "leaving early left the player on %s at state %d." % [ + world.player_cell, world.state.gen1_map_script(world.gen1_safari_gate_byte())]) + _r.check(not world.gen1_safari_active() and not world.event_flag_active(Gen1Layout.SAFARI_GAME_OVER_EVENT), + "leaving early left the game's events standing.") + + ## Red and Blue walk a short purse back down; Yellow's own two routines hand out ## a reduced admission instead, and nothing at all until the fourth empty visit. func _check_the_safari_zone_refuses() -> void: diff --git a/tools/preview_world.gd b/tools/preview_world.gd index c157c321..54822194 100644 --- a/tools/preview_world.gd +++ b/tools/preview_world.gd @@ -19,7 +19,7 @@ const KIND_HELP: Dictionary = { &"battle": "frames, 0: the wild fight preview_battle_request starts, settled past its transition. 1 opens the bag over it and 2 plays the POKé FLUTE from it", &"battle_caught": "frames: the same fight against a species the dex already holds", &"safari": "frames, 0: the Safari game's own battle menu over a Safari Zone map. 1 draws PrintSafariZoneSteps' window on the START menu instead", - &"catch_tutorial": "frames: the Dude's own fight, which answers itself, that many frames in", + &"catch_tutorial": "frames: the Dude's own fight, which answers itself, that many frames in. A Generation 1 cartridge throws the old man's ball, or Prof. Oak's with a second number of 1", &"catch_dex": "none: NewPokedexEntry's page, over the fight the catch that opened it is still in", &"cut": "cell: OWCutAnimation's two halves and the jump shadow", &"fly": "none: FlyFromAnim 80 frames in, with HideSprites' empty OAM behind it", @@ -664,10 +664,19 @@ func _open_battle_bag(frames: int, flute: bool) -> void: ## `CatchTutorial`, played by `DudeAutoInputs` rather than by anybody. The first ## number is how many frames in to photograph: nothing here presses anything. +## A second number of 1 is Yellow's Prof. Oak. func _stage_catch_tutorial() -> void: - _screen.preview_catch_tutorial() + _screen.preview_catch_tutorial(_cell.y == 1) _screen.settle_battle_transition() - _screen.advance_frames(maxi(_cell.x, STAGED_FRAMES)) + if _generation() != RomRegistry.GEN1: + _screen.advance_frames(maxi(_cell.x, STAGED_FRAMES)) + return + ## The tutor's boxes wait on the player, so A is pressed wherever one owes it. + var host: Gen2BattleScreen = _screen.get("_battle_host") + for _frame: int in maxi(_cell.x, STAGED_FRAMES): + if host != null and bool(host.battle_snapshot().get("awaits_press", false)): + _screen.press_button(PokeButton.A) + _screen.advance_frame() ## `DoBattleTransition` over the map it runs on. The first of the two numbers is how diff --git a/tools/preview_world_story.gd b/tools/preview_world_story.gd index 2fbb5527..cca97e50 100644 --- a/tools/preview_world_story.gd +++ b/tools/preview_world_story.gd @@ -28,8 +28,19 @@ const REQUEST_HANDLERS: Dictionary = { &"audio_requested": &"_request_audio", &"magnet_train_requested": &"_request_magnet_train", &"pokedex_entry_requested": &"_request_audio", + &"elevator_requested": &"_request_elevator", + &"vending_requested": &"_request_vending", + &"hall_of_fame_requested": &"_request_hall_of_fame", + &"quick_save_requested": &"_request_acknowledged", + &"soft_reset_requested": &"_request_soft_reset", } +## The drink the next `VendingMachineMenu` buys, as an item number; 0 is B. +var _gen1_drink: int = 0 + +## The map number the next `DisplayElevatorFloorMenu` is answered with; -1 is B. +var _gen1_elevator_floor: int = -1 + ## SPECIALCALL_ASSISTANT (constants/phone_constants.asm), armed by beating ## Falkner and answered by ElmPhoneCallerScript's .assistant branch. const SPECIALCALL_ASSISTANT: int = 3 @@ -8520,6 +8531,7 @@ func _drain_story( "hall_of_fame": _hall_of_fame_events(results), "credits": _credits_events(results), "texts": _gen1_texts(results), + "soft_reset": false, "reason": "", "details": "", } @@ -8546,6 +8558,7 @@ func _drain_story( "credits": state["credits"], "approaches": state["approaches"], "texts": state["texts"], + "soft_reset": state["soft_reset"], "terminal": String(state["reason"]).is_empty() \ and not world.script_input_waiting() and world.pending_runtime_request().is_empty(), "reason": state["reason"], @@ -8597,6 +8610,25 @@ func _runtime_request(world: Gen2WorldAPI, state: Dictionary) -> Array: return (Callable(self, String(REQUEST_HANDLERS[kind]))).call(world, request, state) +## `HallOfFamePC` on Generation 1 is a request rather than Crystal's event, so +## the induction is counted where the event is and the script carries on. +func _request_hall_of_fame(world: Gen2WorldAPI, _request: Dictionary, state: Dictionary) -> Array: + state["hall_of_fame"] = int(state["hall_of_fame"]) + 1 + return world.complete_runtime_request({"ok": true}) + + +## `SaveGameData`: nothing this walk keeps, and the script goes on. +func _request_acknowledged(world: Gen2WorldAPI, _request: Dictionary, _state: Dictionary) -> Array: + return world.complete_runtime_request({"ok": true, "script_value": 1}) + + +## `jp Init` behind the Hall of Fame's save: the console restarts, so the drain +## is over and the settle behind it does not wait for a map script that never runs. +func _request_soft_reset(world: Gen2WorldAPI, _request: Dictionary, state: Dictionary) -> Array: + state["soft_reset"] = true + return world.complete_runtime_request({"ok": true}) + + func _request_rival_name(world: Gen2WorldAPI, _request: Dictionary, state: Dictionary) -> Array: var named: Dictionary = Gen2WorldHost.complete_runtime_request( world, {"ok": true, "name": "SILVER"}, state["save"], false, state["random"] @@ -8639,6 +8671,9 @@ func _request_party_host(world: Gen2WorldAPI, _request: Dictionary, state: Dicti ## answers the request itself instead of opening a battle screen, so it credits ## the same accounts that screen would have. func _request_battle(world: Gen2WorldAPI, request: Dictionary, state: Dictionary) -> Array: + ## A Generation 1 tutor rides a `battle_requested` the map script wrote. + if bool((request.get("values", {}) as Dictionary).get("tutorial", false)): + return _request_catch_tutorial(world, request, state) var data: GameData = state["data"] var save: Gen2SaveData = state["save"] var player_party: Gen2Party = ( @@ -8784,6 +8819,29 @@ func _request_apricorns(world: Gen2WorldAPI, _request: Dictionary, state: Dictio return given.get("results", []) +func _request_elevator(world: Gen2WorldAPI, request: Dictionary, state: Dictionary) -> Array: + var floors: Array = (request.get("values", {}) as Dictionary).get("floors", []) + for row: Dictionary in floors: + if int(row.get("map", -1)) == _gen1_elevator_floor: + _gen1_elevator_floor = -1 + return world.complete_runtime_request({"ok": true, "floor": row}) + if _gen1_elevator_floor >= 0: + return _request_failed(state, "the car lists no floor on map %d" % _gen1_elevator_floor, {"floors": floors}) + return world.complete_runtime_request({"ok": true}) + + +func _request_vending(world: Gen2WorldAPI, request: Dictionary, state: Dictionary) -> Array: + for row: Dictionary in (request.get("values", {}) as Dictionary).get("rows", []): + if int(row.get("item", 0)) != _gen1_drink: + continue + _gen1_drink = 0 + var bought: Dictionary = Gen2WorldMartHost.vend(world, state["save"], row, false) + if not bool(bought.get("ok", false)): + return _request_failed(state, "the machine refused: %s" % bought.get("reason", ""), bought) + break + return world.complete_runtime_request({"ok": true}) + + ## `DisplayPokedex` behind a Generation 1 ball is a page with nothing to answer, ## the way `waitsfx` is. func _request_audio(world: Gen2WorldAPI, _request: Dictionary, _state: Dictionary) -> Array: @@ -8800,6 +8858,8 @@ func _absorb_results(world: Gen2WorldAPI, results: Array, state: Dictionary) -> state["credits"] += _credits_events(results) state["waits"] += 1 _record_failure(results, state) + if bool(state["soft_reset"]): + return true if world.script_input_waiting() or not world.pending_runtime_request().is_empty(): return false for result: Dictionary in results: @@ -8917,7 +8977,8 @@ func _reachable_step( # cell it was asked to reach, which is what makes Ecruteak Gym's thirty # holes a maze instead of open floor. A warp_event on ordinary floor is # inert, as CheckWarpCollision has it, so it is not a wall. - if direct != warp_target and world.warp_pending(direct): + if direct != warp_target and (world.warp_pending(direct) \ + or not world.gen1_dungeon_hole_at(direct).is_empty()): return Vector2i(-1, -1) # A whirlpool traps rather than moves: .CheckTile answers # PLAYERMOVEMENT_FORCE_TURN for the cell the player stands on, so a plan that @@ -8981,7 +9042,7 @@ func _named_items(data: GameData, items: Dictionary) -> Dictionary: ## landfall names one land cell and the step onto it is .ExitWater. func _walk_to_story_cell( world: Gen2WorldAPI, target: Vector2i, water_only: bool = false, - dispatch_target_events: bool = true + dispatch_target_events: bool = true, avoid: Dictionary = {} ) -> Dictionary: if world == null or world.current_map == null: return {"ok": false, "reason": "missing world"} @@ -8990,7 +9051,7 @@ func _walk_to_story_cell( "ok": true, "events": _dispatch_after_step(world, target) if dispatch_target_events else [], } - var plan: Dictionary = _plan_walk(world, target, water_only) + var plan: Dictionary = _plan_walk(world, target, water_only, avoid) if not bool(plan["found"]): return { "ok": false, @@ -9003,7 +9064,9 @@ func _walk_to_story_cell( } var steps: Array[Vector2i] = plan["steps"] var events: Array = [] + var arrows: Dictionary = _gen1_arrow_landings(world) for direction: Vector2i in steps: + var expected: Vector2i = arrows.get(world.player_cell + direction, world.player_cell + direction) var moved: Dictionary = world.move_result(direction) if not bool(moved.get("ok", false)): return { @@ -9013,15 +9076,19 @@ func _walk_to_story_cell( ], } events = _dispatch_after_step(world) - if not events.is_empty(): + ## A spin tile has already moved the player off the plan. + if not events.is_empty() or world.player_cell != expected: break return {"ok": true, "steps": steps.size(), "events": events} -func _plan_walk(world: Gen2WorldAPI, target: Vector2i, water_only: bool = false) -> Dictionary: +func _plan_walk( + world: Gen2WorldAPI, target: Vector2i, water_only: bool = false, avoid: Dictionary = {} +) -> Dictionary: var frontier: Array[Vector2i] = [world.player_cell] var previous: Dictionary = {world.player_cell: {"cell": Vector2i(-1, -1), "direction": Vector2i.ZERO}} var directions: Array[Vector2i] = [Vector2i.UP, Vector2i.DOWN, Vector2i.LEFT, Vector2i.RIGHT] + var arrows: Dictionary = _gen1_arrow_landings(world) var found: bool = false while not frontier.is_empty(): var cell: Vector2i = frontier.pop_front() @@ -9032,7 +9099,9 @@ func _plan_walk(world: Gen2WorldAPI, target: Vector2i, water_only: bool = false) var next: Vector2i = _reachable_step( world, cell, direction, target, water_only and cell + direction != target ) - if next.x < 0 or previous.has(next): + ## A spin tile is one edge to wherever its ride stops. + next = arrows.get(next, next) + if next.x < 0 or previous.has(next) or (next != target and avoid.has(next)): continue previous[next] = {"cell": cell, "direction": direction} frontier.append(next) @@ -9045,6 +9114,34 @@ func _plan_walk(world: Gen2WorldAPI, target: Vector2i, water_only: bool = false) return {"found": found, "steps": steps, "previous": previous} +## Each spin tile and the cell `DecodeArrowMovementRLE`'s whole ride stops on. +func _gen1_arrow_landings(world: Gen2WorldAPI) -> Dictionary: + var out: Dictionary = {} + if world == null or not world.is_gen1() or world.current_map == null: + return out + var pending: Array = [world.current_map.scripts.get("states", [])] + while not pending.is_empty(): + var nodes: Variant = pending.pop_back() + if not nodes is Array: + continue + for node: Variant in nodes as Array: + if not node is Dictionary: + continue + if node.has("nodes"): + pending.append(node["nodes"]) + for key: String in Gen1Layout.SCRIPT_BRANCH_KEYS: + if node.has(key): + pending.append(node[key]) + if String(node.get("op", "")) != "arrow_movement": + continue + for cell: Dictionary in node["cells"] as Array: + var landing := Vector2i(int(cell["x"]), int(cell["y"])) + for leg: Dictionary in cell["moves"] as Array: + landing += world.movement_direction(int(leg["direction"])) * int(leg["steps"]) + out[Vector2i(int(cell["x"]), int(cell["y"]))] = landing + return out + + ## What a failed walk hit, when what it hit was somebody standing there. ## `Gen2WorldAPI.can_walk_to()` refuses a cell an object holds exactly as it refuses ## a wall, so without this a route blocked by an NPC or an item ball reads the same @@ -9302,7 +9399,10 @@ const GEN1_MT_MOON_NERD_CELL := Vector2i(13, 8) const GEN1_MT_MOON_BELOW_DOME := Vector2i(12, 7) const GEN1_MT_MOON_B2F_EXIT_LADDER := Vector2i(5, 7) const GEN1_MT_MOON_B1F_EXIT := Vector2i(27, 3) -const GEN1_EVENT_GOT_DOME_FOSSIL: int = 0x570 + 14 +## Yellow's Mt. Moon run puts it right behind the 1F trainers. +const GEN1_EVENT_GOT_DOME_FOSSIL: Dictionary = { + &"red": 0x570 + 14, &"blue": 0x570 + 14, &"yellow": 0x570 + 8, +} const GEN1_ROUTE_24: int = 35 const GEN1_ROUTE_25: int = 36 const GEN1_BILLS_HOUSE: int = 88 @@ -9311,6 +9411,7 @@ const GEN1_CERULEAN_BRIDGE_FOOT := Vector2i(20, 6) const GEN1_CERULEAN_GYM_DOOR := Vector2i(30, 19) const GEN1_BELOW_BILL_POKEMON := Vector2i(6, 6) const GEN1_BILLS_PC_CELL := Vector2i(1, 5) +const GEN1_BELOW_DAMIAN := Vector2i(6, 6) const GEN1_BELOW_BILL := Vector2i(4, 5) const GEN1_MISTY := Vector2i(4, 2) const GEN1_EVENT_BEAT_CERULEAN_RIVAL: int = 152 @@ -9352,6 +9453,368 @@ const GEN1_EVENT_GOT_TM24: int = 358 const GEN1_EVENT_BEAT_LT_SURGE: int = 359 const GEN1_EVENT_2ND_LOCK_OPENED: int = 352 const GEN1_BIT_THUNDERBADGE: int = 2 +const GEN1_ROUTE_9: int = 20 +const GEN1_ROUTE_10: int = 21 +const GEN1_ROCK_TUNNEL_1F: int = 82 +const GEN1_ROCK_TUNNEL_B1F: int = 232 +const GEN1_LAVENDER_TOWN: int = 4 +const GEN1_ROUTE_8: int = 19 +const GEN1_ROUTE_7: int = 18 +const GEN1_UNDERGROUND_PATH_ROUTE_8: int = 80 +const GEN1_UNDERGROUND_PATH_ROUTE_7: int = 77 +const GEN1_UNDERGROUND_PATH_WEST_EAST: int = 121 +const GEN1_CELADON_CITY: int = 6 +const GEN1_CELADON_GYM: int = 134 +## The tree behind Route 9's west edge, and the one `CeladonGym`'s corridor ends on. +const GEN1_ROUTE_9_TREE_APPROACH := Vector2i(4, 8) +## `ReplaceTreeTileBlock` edits the tilemap alone, so the gym's tree stands +## again on the way out and is cut from inside the fence. +const GEN1_VERMILION_TREE_INSIDE := Vector2i(15, 19) +const GEN1_CELADON_GYM_TREE_APPROACH := Vector2i(5, 8) +## The tree at (35, 32), which seals the gym's own corner of Celadon City. +const GEN1_CELADON_TREE_APPROACH := Vector2i(35, 31) +const GEN1_CELADON_TREE_INSIDE := Vector2i(35, 33) +## Rock Tunnel's four ladders in walking order, each a floor and its cell, and +## the south exit: `TilePairCollisionsLand`'s CAVERN rows are what make the +## floors a maze rather than open rock. +const GEN1_ROCK_TUNNEL_LADDERS: Array = [ + [GEN1_ROCK_TUNNEL_B1F, Vector2i(37, 3)], [GEN1_ROCK_TUNNEL_1F, Vector2i(27, 3)], + [GEN1_ROCK_TUNNEL_B1F, Vector2i(17, 11)], [GEN1_ROCK_TUNNEL_1F, Vector2i(3, 3)], +] +const GEN1_ROCK_TUNNEL_SOUTH_EXIT := Vector2i(15, 33) +const GEN1_ROUTE_8_UNDERGROUND := Vector2i(13, 3) +const GEN1_CELADON_GYM_DOOR := Vector2i(12, 27) +const GEN1_ERIKA := Vector2i(4, 3) +const GEN1_EVENT_GOT_TM21: int = 424 +const GEN1_EVENT_BEAT_ERIKA: int = 425 +const GEN1_BIT_RAINBOWBADGE: int = 3 +const GEN1_GAME_CORNER: int = 135 +const GEN1_ROCKET_HIDEOUT_B1F: int = 199 +const GEN1_ROCKET_HIDEOUT_B2F: int = 200 +const GEN1_ROCKET_HIDEOUT_B3F: int = 201 +const GEN1_ROCKET_HIDEOUT_B4F: int = 202 +const GEN1_ROCKET_HIDEOUT_ELEVATOR: int = 203 +const GEN1_GAME_CORNER_DOOR := Vector2i(28, 19) +## The poster's guard, and the stairs `GameCornerPosterText` writes in. +const GEN1_GAME_CORNER_ROCKET := Vector2i(9, 5) +const GEN1_GAME_CORNER_STAIRS := Vector2i(17, 4) +const GEN1_HIDEOUT_B1F_STAIRS := Vector2i(23, 2) +const GEN1_HIDEOUT_B2F_STAIRS := Vector2i(21, 8) +const GEN1_HIDEOUT_B3F_STAIRS := Vector2i(19, 18) +const GEN1_HIDEOUT_B4F_STAIRS := Vector2i(19, 10) +const GEN1_HIDEOUT_B3F_UP := Vector2i(25, 6) +const GEN1_HIDEOUT_B2F_ELEVATOR := Vector2i(24, 19) +## `RocketHideoutB4FRocket3` drops the LIFT KEY's ball beside him. +const GEN1_HIDEOUT_ROCKET_3 := Vector2i(11, 2) +const GEN1_BELOW_LIFT_KEY := Vector2i(10, 3) +const GEN1_HIDEOUT_CAR_PANEL := Vector2i(1, 2) +const GEN1_HIDEOUT_CAR_DOOR := Vector2i(2, 1) +## The two Rockets `RocketHideoutB4F`'s load callback opens the door on. +const GEN1_HIDEOUT_DOOR_ROCKETS: Array = [Vector2i(23, 12), Vector2i(26, 12)] +const GEN1_HIDEOUT_GIOVANNI := Vector2i(25, 3) +const GEN1_BELOW_SILPH_SCOPE := Vector2i(25, 3) +const GEN1_LIFT_KEY: int = 0x4A +const GEN1_SILPH_SCOPE: int = 0x48 +const GEN1_EVENT_FOUND_ROCKET_HIDEOUT: int = 441 +const GEN1_EVENT_ROCKET_DROPPED_LIFT_KEY: int = 1702 +const GEN1_EVENT_BEAT_ROCKET_HIDEOUT_GIOVANNI: int = 1703 +const GEN1_POKEMON_TOWER_1F: int = 142 +const GEN1_POKEMON_TOWER_2F: int = 143 +const GEN1_POKEMON_TOWER_3F: int = 144 +const GEN1_POKEMON_TOWER_4F: int = 145 +const GEN1_POKEMON_TOWER_5F: int = 146 +const GEN1_POKEMON_TOWER_6F: int = 147 +const GEN1_POKEMON_TOWER_7F: int = 148 +const GEN1_MR_FUJIS_HOUSE: int = 149 +const GEN1_HIDEOUT_B4F_ELEVATOR := Vector2i(24, 15) +const GEN1_HIDEOUT_B1F_ELEVATOR_STAIRS := Vector2i(21, 2) +## `RocketHideoutB1FDoorCallbackScript` shuts the car's pocket on this Rocket. +const GEN1_HIDEOUT_B1F_DOOR_ROCKET := Vector2i(28, 18) +const GEN1_LAVENDER_TOWER_DOOR := Vector2i(14, 5) +## The tower's stairs alternate sides; 6F's RARE CANDY ball plugs the one-cell +## gap on the way to the 7F stairs, and the ghost MAROWAK stands on (10, 16). +const GEN1_TOWER_EAST_STAIRS := Vector2i(18, 9) +const GEN1_TOWER_WEST_STAIRS := Vector2i(3, 9) +const GEN1_ABOVE_RARE_CANDY := Vector2i(6, 7) +const GEN1_TOWER_7F_STAIRS := Vector2i(9, 16) +const GEN1_MR_FUJI := Vector2i(10, 3) +const GEN1_BELOW_MR_FUJI_HOME := Vector2i(3, 2) +const GEN1_POKE_FLUTE: int = 0x49 +const GEN1_EVENT_BEAT_POKEMON_TOWER_RIVAL: int = 239 +const GEN1_EVENT_BEAT_GHOST_MAROWAK: int = 271 +const GEN1_EVENT_GOT_POKE_FLUTE: int = 296 +const GEN1_EVENT_RESCUED_MR_FUJI: int = 1231 +const GEN1_ROUTE_12: int = 23 +const GEN1_ROUTE_13: int = 24 +const GEN1_ROUTE_14: int = 25 +const GEN1_ROUTE_15: int = 26 +const GEN1_FUCHSIA_CITY: int = 7 +const GEN1_FUCHSIA_GYM: int = 157 +const GEN1_ROUTE_12_GATE_1F: int = 87 +## Route 13's rows 4 and 6 end at Bird Keepers; row 8 goes through. +const GEN1_ROUTE_13_WEST_EDGE := Vector2i(0, 8) +const GEN1_ROUTE_15_GATE_1F: int = 184 +const GEN1_ROUTE_12_GATE_DOOR := Vector2i(10, 15) +const GEN1_ROUTE_12_GATE_SOUTH := Vector2i(4, 7) +const GEN1_ROUTE_15_GATE_DOOR := Vector2i(14, 8) +const GEN1_ROUTE_15_GATE_WEST := Vector2i(0, 4) +const GEN1_ROUTE_15_WEST_DOOR := Vector2i(7, 8) +const GEN1_ROUTE_15_GATE_EAST_EXIT := Vector2i(7, 4) +const GEN1_ROUTE_14_EAST_EDGE := Vector2i(19, 8) +const GEN1_ROUTE_12_GATE_SOUTH_DOOR := Vector2i(10, 21) +const GEN1_ROUTE_12_GATE_NORTH := Vector2i(4, 0) +const GEN1_ABOVE_SNORLAX := Vector2i(10, 61) +const GEN1_FUCHSIA_GYM_DOOR := Vector2i(5, 27) +const GEN1_KOGA := Vector2i(4, 10) +const GEN1_EVENT_FIGHT_ROUTE12_SNORLAX: int = 1166 +const GEN1_EVENT_BEAT_ROUTE12_SNORLAX: int = 1167 +const GEN1_EVENT_GOT_TM06: int = 600 +const GEN1_EVENT_BEAT_KOGA: int = 601 +const GEN1_BIT_SOULBADGE: int = 4 +const GEN1_SAFARI_ZONE_GATE: int = 156 +const GEN1_SAFARI_ZONE_NORTH: int = 218 +const GEN1_SAFARI_ZONE_WEST: int = 219 +const GEN1_SAFARI_ZONE_CENTER: int = 220 +const GEN1_SAFARI_ZONE_SECRET_HOUSE: int = 222 +const GEN1_WARDENS_HOUSE: int = 155 +const GEN1_SAFARI_GATE_DOOR := Vector2i(18, 3) +const GEN1_SAFARI_GATE_OFFER := Vector2i(3, 2) +## The centre's tree lines leave the entrance the east door alone. +const GEN1_SAFARI_ZONE_EAST: int = 217 +const GEN1_SAFARI_CENTER_EAST := Vector2i(29, 10) +const GEN1_SAFARI_EAST_NORTH := Vector2i(0, 4) +const GEN1_SAFARI_NORTH_WEST := Vector2i(2, 35) +const GEN1_SAFARI_WEST_NORTH := Vector2i(20, 0) +const GEN1_SAFARI_NORTH_EAST := Vector2i(39, 30) +const GEN1_SAFARI_EAST_CENTER := Vector2i(0, 22) +const GEN1_SAFARI_SECRET_HOUSE_DOOR := Vector2i(3, 3) +const GEN1_BELOW_FISHING_GURU := Vector2i(3, 4) +const GEN1_BELOW_GOLD_TEETH := Vector2i(19, 8) +const GEN1_SAFARI_CENTER_GATE := Vector2i(14, 25) +const GEN1_SAFARI_GATE_SOUTH := Vector2i(3, 5) +const GEN1_WARDENS_HOUSE_DOOR := Vector2i(27, 27) +const GEN1_WARDEN := Vector2i(2, 3) +const GEN1_HM03: int = 0xC6 +const GEN1_HM04: int = 0xC7 +const GEN1_GOLD_TEETH: int = 0x40 +const GEN1_EVENT_GOT_HM03: int = 2176 +const GEN1_EVENT_GOT_HM04: int = 568 +const GEN1_EVENT_GAVE_GOLD_TEETH: int = 569 +const GEN1_CELADON_MART_1F: int = 122 +const GEN1_CELADON_MART_ROOF: int = 126 +const GEN1_CELADON_MART_ELEVATOR: int = 127 +const GEN1_CELADON_MART_5F: int = 136 +const GEN1_ROUTE_7_GATE: int = 76 +const GEN1_SAFFRON_CITY: int = 10 +const GEN1_SAFFRON_GYM: int = 178 +const GEN1_SILPH_CO_1F: int = 181 +const GEN1_SILPH_CO_2F: int = 207 +const GEN1_SILPH_CO_3F: int = 208 +const GEN1_SILPH_CO_4F: int = 209 +const GEN1_SILPH_CO_5F: int = 210 +const GEN1_SILPH_CO_7F: int = 212 +const GEN1_SILPH_CO_9F: int = 233 +const GEN1_SILPH_CO_11F: int = 235 +const GEN1_CELADON_MART_DOOR := Vector2i(8, 13) +const GEN1_MART_ELEVATOR_DOOR := Vector2i(1, 1) +const GEN1_MART_CAR_PANEL := Vector2i(3, 1) +const GEN1_MART_CAR_DOOR := Vector2i(1, 3) +const GEN1_MART_5F_ROOF_STAIRS := Vector2i(12, 1) +const GEN1_BELOW_VENDING_MACHINE := Vector2i(10, 2) +const GEN1_MART_ROOF_STAIRS := Vector2i(15, 2) +const GEN1_MART_1F_DOOR := Vector2i(2, 7) +const GEN1_ROUTE_7_GATE_WEST := Vector2i(11, 10) +const GEN1_ROUTE_7_GATE_EAST := Vector2i(5, 3) +const GEN1_SILPH_CO_DOOR := Vector2i(18, 21) +## Silph Co.'s stairs alternate between the two top-row cells floor by floor. +const GEN1_SILPH_STAIRS_RIGHT := Vector2i(26, 0) +const GEN1_SILPH_STAIRS_LEFT := Vector2i(24, 0) +## 5F's CARD KEY lies on the bottom row a Rocket seals from the corridor, so the +## teleporter beside him is ridden to 9F and back to land past him. +const GEN1_SILPH_5F_PAD_TO_9F := Vector2i(9, 15) +const GEN1_SILPH_9F_PAD := Vector2i(17, 15) +const GEN1_BESIDE_CARD_KEY := Vector2i(20, 16) +const GEN1_SILPH_5F_PAD_TO_3F := Vector2i(11, 5) +const GEN1_SILPH_3F_DOOR_SIDE := Vector2i(18, 8) +const GEN1_SILPH_3F_PAD_TO_7F := Vector2i(11, 11) +const GEN1_SILPH_LAPRAS_WORKER := Vector2i(1, 5) +const GEN1_DEX_LAPRAS: int = 131 +const GEN1_SILPH_7F_PAD_TO_11F := Vector2i(5, 7) +const GEN1_SILPH_11F_DOOR_SIDE := Vector2i(6, 14) +const GEN1_SILPH_GIOVANNI := Vector2i(6, 9) +const GEN1_SILPH_PRESIDENT := Vector2i(7, 5) +## 11F's car stands behind walls the pad's side never reaches, so the pads are +## ridden back to 3F and its car taken. +const GEN1_SILPH_11F_PAD_TO_7F := Vector2i(3, 2) +const GEN1_SILPH_7F_PAD_TO_3F := Vector2i(5, 3) +const GEN1_SILPH_3F_ELEVATOR := Vector2i(20, 0) +const GEN1_SILPH_CAR_PANEL := Vector2i(3, 1) +const GEN1_SILPH_CAR_DOOR := Vector2i(1, 3) +const GEN1_SILPH_CO_ELEVATOR: int = 236 +const GEN1_SAFFRON_GYM_DOOR := Vector2i(34, 3) +## The pads from the gym's door to Sabrina's room, in riding order. +const GEN1_SAFFRON_GYM_PADS: Array = [ + Vector2i(11, 15), Vector2i(15, 15), Vector2i(15, 5), Vector2i(1, 5), +] +## And from her room back to the door. +const GEN1_SAFFRON_GYM_PADS_OUT: Array = [ + Vector2i(11, 11), Vector2i(5, 5), Vector2i(5, 11), Vector2i(5, 15), Vector2i(19, 17), +] +const GEN1_SABRINA := Vector2i(9, 8) +const GEN1_FRESH_WATER: int = 0x3C +const GEN1_CARD_KEY: int = 0x30 +const GEN1_MASTER_BALL: int = 0x01 +const GEN1_EVENT_BEAT_SILPH_CO_RIVAL: int = 1856 +const GEN1_EVENT_GOT_MASTER_BALL: int = 1933 +const GEN1_EVENT_BEAT_SILPH_CO_GIOVANNI: int = 1935 +const GEN1_EVENT_GOT_TM46: int = 864 +const GEN1_EVENT_BEAT_SABRINA: int = 865 +const GEN1_BIT_MARSHBADGE: int = 5 +const GEN1_ROUTE_8_GATE: int = 79 +const GEN1_ROUTE_8_GATE_WEST := Vector2i(1, 10) +const GEN1_ROUTE_8_GATE_EAST := Vector2i(5, 3) +const GEN1_ROUTE_19: int = 30 +const GEN1_ROUTE_20: int = 31 +const GEN1_CINNABAR_ISLAND: int = 8 +const GEN1_SEAFOAM_ISLANDS_1F: int = 192 +const GEN1_SEAFOAM_ISLANDS_B1F: int = 159 +const GEN1_SEAFOAM_ISLANDS_B2F: int = 160 +const GEN1_SEAFOAM_ISLANDS_B3F: int = 161 +## Yellow's Route 19 keeps a beach house and two more rows of sand. +const GEN1_ROUTE_19_SHORE: Dictionary = { + &"red": Vector2i(8, 9), &"blue": Vector2i(8, 9), &"yellow": Vector2i(8, 11), +} +## A rock column at x = 62 parts Route 20's two seas below the islands' north +## field, so the sea from Fuchsia lands there and reaches the west door alone; +## the east door's own beach opens on the sea to Cinnabar. +const GEN1_ROUTE_20_NORTH_FIELD := Vector2i(61, 3) +const GEN1_ROUTE_20_SEAFOAM_WEST := Vector2i(48, 5) +const GEN1_SEAFOAM_1F_WEST_LADDER := Vector2i(7, 5) +const GEN1_SEAFOAM_B1F_WEST_LADDER := Vector2i(13, 7) +const GEN1_SEAFOAM_B2F_WEST_LADDER := Vector2i(5, 13) +## `TilePairCollisionsWater`'s CAVERN row keeps $05 banks off the pool, so the +## way across B3F is the one $15 bank on each side. +const GEN1_SEAFOAM_B3F_WEST_BANK := Vector2i(15, 7) +const GEN1_SEAFOAM_B3F_SHORE := Vector2i(23, 9) +const GEN1_SEAFOAM_B2F_EAST_LADDER := Vector2i(25, 14) +const GEN1_SEAFOAM_B1F_EAST_LADDER := Vector2i(25, 11) +const GEN1_SEAFOAM_1F_EAST_LADDER := Vector2i(23, 15) +const GEN1_SEAFOAM_1F_EAST_DOOR := Vector2i(26, 17) +const GEN1_ROUTE_20_EAST_BEACH := Vector2i(58, 11) +const GEN1_POKEMON_MANSION_1F: int = 165 +const GEN1_POKEMON_MANSION_2F: int = 214 +const GEN1_POKEMON_MANSION_3F: int = 215 +const GEN1_POKEMON_MANSION_B1F: int = 216 +const GEN1_CINNABAR_SHORE := Vector2i(19, 13) +const GEN1_MANSION_DOOR := Vector2i(6, 3) +const GEN1_MANSION_1F_STAIRS := Vector2i(5, 10) +const GEN1_MANSION_2F_STAIRS := Vector2i(6, 1) +## Each statue's own cell is one up from where it is pressed. +const GEN1_MANSION_3F_SWITCH := Vector2i(10, 6) +const GEN1_MANSION_3F_HOLE := Vector2i(16, 14) +const GEN1_MANSION_1F_B1F_STAIRS := Vector2i(21, 23) +const GEN1_MANSION_B1F_SOUTH_SWITCH := Vector2i(18, 26) +const GEN1_MANSION_B1F_NORTH_SWITCH := Vector2i(20, 4) +const GEN1_MANSION_SECRET_KEY := Vector2i(5, 13) +const GEN1_MANSION_B1F_STAIRS := Vector2i(23, 22) +const GEN1_MANSION_EAST_DOOR := Vector2i(26, 27) +const GEN1_SECRET_KEY: int = 0x2B +const GEN1_EVENT_MANSION_SWITCH_ON: int = 632 +const GEN1_NUGGET: int = 0x31 +const GEN1_CINNABAR_GYM: int = 166 +const GEN1_CINNABAR_GYM_DOOR := Vector2i(18, 3) +## Each machine's cell below it and `hGymGateAnswer`, the `wCurrentMenuItem` it +## wants: 0 is YES. +const GEN1_CINNABAR_QUIZ: Array = [ + [Vector2i(15, 8), 0], [Vector2i(10, 2), 1], [Vector2i(9, 8), 1], + [Vector2i(9, 14), 1], [Vector2i(1, 14), 0], [Vector2i(1, 8), 1], +] +const GEN1_EVENT_CINNABAR_GYM_GATE0_UNLOCKED: int = 680 +const GEN1_BLAINE := Vector2i(3, 3) +const GEN1_EVENT_BEAT_BLAINE: int = 665 +const GEN1_EVENT_GOT_TM38: int = 664 +const GEN1_BIT_VOLCANOBADGE: int = 6 +const GEN1_ROUTE_21: int = 32 +const GEN1_VIRIDIAN_GYM: int = 45 +const GEN1_CINNABAR_WEST_SHORE := Vector2i(4, 4) +const GEN1_PALLET_SHORE := Vector2i(8, 15) +const GEN1_TM11: int = 0xD3 +const GEN1_VIRIDIAN_GYM_DOOR := Vector2i(32, 7) +const GEN1_VIRIDIAN_GIOVANNI := Vector2i(2, 1) +const GEN1_EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI: int = 81 +const GEN1_EVENT_GOT_TM27: int = 80 +const GEN1_BIT_EARTHBADGE: int = 7 +const GEN1_ROUTE_22: int = 33 +const GEN1_ROUTE_23: int = 34 +const GEN1_ROUTE_22_GATE: int = 193 +const GEN1_VICTORY_ROAD_1F: int = 108 +const GEN1_VICTORY_ROAD_2F: int = 194 +const GEN1_VICTORY_ROAD_3F: int = 198 +const GEN1_ROUTE_22_GATE_DOOR := Vector2i(8, 5) +const GEN1_ROUTE_22_GATE_NORTH := Vector2i(4, 0) +const GEN1_ROUTE_23_SOUTH_BANK := Vector2i(11, 104) +const GEN1_ROUTE_23_NORTH_BANK := Vector2i(8, 71) +const GEN1_ROUTE_23_VICTORY_ROAD_DOOR := Vector2i(4, 31) +## 1F's boulder to its switch at (17, 13): down to the entrance row, along it, +## up column 9 from the door cell (whose edge is never faced), along row 14, up +## column 16, right and down. Each row is a step name, the cell pushed from, the +## way and how many times. +const GEN1_VICTORY_ROAD_1F_PUSHES: Array = [ + ["victory_road_1f_push_down", Vector2i(5, 14), Vector2i.DOWN, 1], + ["victory_road_1f_push_right", Vector2i(4, 16), Vector2i.RIGHT, 4], + ["victory_road_1f_push_up", Vector2i(9, 17), Vector2i.UP, 2], + ["victory_road_1f_push_along", Vector2i(8, 14), Vector2i.RIGHT, 7], + ["victory_road_1f_push_up_again", Vector2i(16, 15), Vector2i.UP, 2], + ["victory_road_1f_push_right_again", Vector2i(15, 12), Vector2i.RIGHT, 1], + ["victory_road_1f_push_onto_switch", Vector2i(17, 11), Vector2i.DOWN, 1], +] +const GEN1_EVENT_VICTORY_ROAD_1_BOULDER_ON_SWITCH: int = 2327 +const GEN1_VICTORY_ROAD_1F_LADDER := Vector2i(1, 1) +## 2F's first switch at (1, 16), which opens the way east: the boulder at (4, 14) +## left, down column 3 and left along the bottom row. +const GEN1_VICTORY_ROAD_2F_PUSHES: Array = [ + ["victory_road_2f_push_left", Vector2i(5, 14), Vector2i.LEFT, 1], + ["victory_road_2f_push_down", Vector2i(3, 13), Vector2i.DOWN, 2], + ["victory_road_2f_push_onto_switch", Vector2i(4, 16), Vector2i.LEFT, 2], +] +const GEN1_EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH1: int = 1336 +const GEN1_EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH2: int = 1343 +## 3F's boulder into its hole, which `VictoryRoad3FDefaultScript` answers by +## showing 2F's own on (23, 16), and the player after it: that one goes left +## along the bottom row to the second switch at (9, 16), which opens the way to +## the east ladder's own $05 floor. +const GEN1_VICTORY_ROAD_2F_LADDER := Vector2i(23, 7) +## The boulder standing in the wall's one gap at (13, 12), pushed out of it and +## once more off the cell below. +const GEN1_VICTORY_ROAD_3F_GAP_ABOVE := Vector2i(13, 11) +const GEN1_VICTORY_ROAD_3F_HOLE_SIDE := Vector2i(21, 15) +const GEN1_VICTORY_ROAD_3F_HOLE := Vector2i(23, 15) +const GEN1_VICTORY_ROAD_2F_FALLEN_BOULDER_SIDE := Vector2i(24, 16) +const GEN1_VICTORY_ROAD_2F_SECOND_PUSHES: int = 14 +const GEN1_VICTORY_ROAD_2F_EAST_LADDER := Vector2i(25, 14) +const GEN1_VICTORY_ROAD_3F_LADDER_DOWN := Vector2i(26, 8) +const GEN1_VICTORY_ROAD_2F_EXIT := Vector2i(29, 8) +const GEN1_INDIGO_PLATEAU: int = 9 +const GEN1_INDIGO_PLATEAU_LOBBY: int = 174 +const GEN1_INDIGO_PLATEAU_DOOR := Vector2i(9, 5) +const GEN1_LORELEIS_ROOM: int = 245 +const GEN1_BRUNOS_ROOM: int = 246 +const GEN1_AGATHAS_ROOM: int = 247 +const GEN1_LANCES_ROOM: int = 113 +const GEN1_CHAMPIONS_ROOM: int = 120 +const GEN1_LOBBY_STAIRS := Vector2i(8, 0) +## Each of the first three rooms is the same: the member at (5, 2), the door on. +const GEN1_ELITE_MEMBER := Vector2i(5, 2) +const GEN1_ELITE_NEXT_DOOR := Vector2i(4, 0) +const GEN1_LANCE := Vector2i(6, 1) +const GEN1_EVENT_BEAT_LANCE: int = 2302 +const GEN1_LANCES_ROOM_EXIT := Vector2i(5, 0) +const GEN1_HALL_OF_FAME: int = 118 +const GEN1_ELITE_FOUR: Array = [ + ["lorelei", GEN1_BRUNOS_ROOM, 2273, "EVENT_BEAT_LORELEIS_ROOM_TRAINER_0"], + ["bruno", GEN1_AGATHAS_ROOM, 2281, "EVENT_BEAT_BRUNOS_ROOM_TRAINER_0"], + ["agatha", GEN1_LANCES_ROOM, 2289, "EVENT_BEAT_AGATHAS_ROOM_TRAINER_0"], +] const GEN1_BEDROOM_STAIRS := Vector2i(7, 1) const GEN1_HOUSE_DOOR := Vector2i(2, 7) ## `PalletTownDefaultScript` stops the player at `wYCoord == 1`, and Yellow's at 0. @@ -9360,7 +9823,11 @@ const GEN1_PALLET_NORTH_PATH: Dictionary = { } const GEN1_PALLET_LAB_DOOR := Vector2i(12, 11) const GEN1_LAB_DOOR := Vector2i(4, 11) -const GEN1_LAB_BELOW_CHARMANDER := Vector2i(6, 4) +## The cell under the ball the walk takes and the dex number it gives: Yellow's +## one EEVEE ball sends the rival for it and walks the player to Oak's PIKACHU. +const GEN1_LAB_STARTER_BALLS: Dictionary = { + &"red": [Vector2i(6, 4), 4], &"blue": [Vector2i(6, 4), 4], &"yellow": [Vector2i(7, 4), 25], +} const GEN1_LAB_BELOW_OAK := Vector2i(5, 3) const GEN1_VIRIDIAN_MART_DOOR := Vector2i(29, 19) const GEN1_MART_DOOR := Vector2i(3, 7) @@ -9375,7 +9842,6 @@ const GEN1_EVENT_OAK_GOT_PARCEL: int = 56 const GEN1_EVENT_GOT_OAKS_PARCEL: int = 57 const GEN1_EVENT_GOT_TM34: int = 118 const GEN1_EVENT_BEAT_BROCK: int = 119 -const GEN1_CHARMANDER_DEX: int = 4 const GEN1_OAKS_PARCEL: int = 0x46 const GEN1_TM34: int = 0xEA const GEN1_BIT_BOULDERBADGE: int = 0 @@ -9428,7 +9894,11 @@ func _gen1_story_path(data: GameData) -> Dictionary: _gen1_pokedex_leg, _gen1_viridian_forest_leg, _gen1_pewter_gym_leg, _gen1_mt_moon_leg, _gen1_cerulean_rival_leg, _gen1_bills_house_leg, _gen1_cerulean_gym_leg, _gen1_cerulean_thief_leg, _gen1_ss_anne_leg, - _gen1_vermilion_gym_leg, + _gen1_vermilion_gym_leg, _gen1_rock_tunnel_leg, _gen1_celadon_gym_leg, + _gen1_rocket_hideout_leg, _gen1_pokemon_tower_leg, _gen1_fuchsia_gym_leg, + _gen1_safari_zone_leg, _gen1_saffron_drink_leg, _gen1_silph_co_leg, + _gen1_saffron_gym_leg, _gen1_cinnabar_leg, _gen1_mansion_leg, _gen1_cinnabar_gym_leg, + _gen1_viridian_gym_leg, _gen1_route_23_leg, _gen1_victory_road_leg, _gen1_elite_four_leg, ] for leg: Callable in legs: var walked: Dictionary = leg.call(world, save, random, data, path) @@ -9449,7 +9919,7 @@ func _gen1_story_path(data: GameData) -> Dictionary: ## a warp takes it, as `CheckWarpsNoCollision` does under a scripted walk. func _gen1_settle( world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, - results: Array = [] + results: Array = [], answers: Array[int] = [] ) -> Dictionary: var statuses: Array = [] var battles: Array = [] @@ -9457,7 +9927,8 @@ func _gen1_settle( var warps: int = 0 for pass_index: int in GEN1_SETTLE_PASSES: if not results.is_empty(): - var run: Dictionary = _drain_story(world, results, save, random, data) + var run: Dictionary = _drain_story(world, results, save, random, data, false, answers) + answers = [] texts.append_array(run.get("texts", [])) statuses.append_array(run.get("statuses", [])) battles.append_array(run.get("battles", [])) @@ -9466,15 +9937,26 @@ func _gen1_settle( "ok": false, "reason": run["reason"], "details": run.get("details", ""), "passes": pass_index, "texts": texts, } + ## `jp Init`: the console restarts and no map script runs again. + if bool(run.get("soft_reset", false)): + return { + "ok": true, "passes": pass_index + 1, "statuses": statuses, + "battles": battles, "texts": texts, "warps": warps, "soft_reset": true, + } var stepping: bool = world.player_step_in_progress() ## A state that only writes the next one shows nothing on its own pass. var script_state: int = world.gen1_map_script_state() world.advance_script_wait_frame() world.advance_player_step_pass() world.advance_scripted_steps_pass() - if stepping and not world.player_step_in_progress() and world.warp_pending(): - if bool(world.try_warp().get("ok", false)): - warps += 1 + if stepping and not world.player_step_in_progress(): + if world.warp_pending(): + if bool(world.try_warp().get("ok", false)): + warps += 1 + ## `RunMapScript` sets the dungeon warp bit, read behind `CheckWarpsNoCollision`. + elif not world.gen1_dungeon_hole_at(world.player_cell).is_empty(): + if bool(world.gen1_dungeon_fall().get("ok", false)): + warps += 1 results = world.dispatch_sight_events() if results.is_empty() and _gen1_settled(world) \ and world.gen1_map_script_state() == script_state: @@ -9517,9 +9999,17 @@ func _gen1_walk( ) -> Dictionary: var runs: Array = [] var from: Vector2i = world.map_id() + ## A cell whose script only moved the player off it is planned around next + ## time, which is how Cinnabar's locked gym door is passed by the row below + ## it; a scene that fought first, like Mt. Moon's Rockets, is over. + var avoid: Dictionary = {} for _attempt: int in WALK_RESOLVE_ATTEMPTS: - var walked: Dictionary = _walk_to_story_cell(world, cell) + var walked: Dictionary = _walk_to_story_cell(world, cell, _gen1_surfing(world), true, avoid) + var interrupted: Vector2i = world.player_cell var settled: Dictionary = _gen1_settle(world, save, random, data, walked.get("events", [])) + if world.player_cell != interrupted and world.map_id() == from \ + and settled.get("battles", []).is_empty(): + avoid[interrupted] = true if not settled.get("texts", []).is_empty() or not settled.get("battles", []).is_empty(): runs.append({"cell": _cell_value(world), "texts": settled.get("texts", []), "battles": settled.get("battles", [])}) @@ -9527,8 +10017,10 @@ func _gen1_walk( return {"ok": false, "reason": settled.get("reason", ""), "details": settled.get("details", ""), "runs": runs} if not bool(walked.get("ok", false)): return {"ok": false, "reason": walked.get("reason", ""), "runs": runs} - if world.player_cell == cell or world.map_id() != from: - return {"ok": true, "runs": runs} + ## A pad on the same map lands elsewhere on it, as Saffron Gym's do. + var warped: bool = world.map_id() != from or int(settled.get("warps", 0)) > 0 + if world.player_cell == cell or warped: + return {"ok": true, "runs": runs, "warped": warped} return {"ok": false, "reason": "walk to %s kept being interrupted" % cell, "runs": runs} @@ -9537,10 +10029,19 @@ func _gen1_warp_walk( data: GameData ) -> Dictionary: var from: Vector2i = world.map_id() + ## A pad the player landed on is taken again by stepping off and back on, + ## which is how Silph Co.'s teleporters are ridden twice. + if world.player_cell == cell: + for direction: Vector2i in [Vector2i.DOWN, Vector2i.UP, Vector2i.LEFT, Vector2i.RIGHT]: + if world.can_walk_to(cell + direction) and not world.warp_pending(cell + direction): + var aside: Dictionary = _gen1_walk(world, cell + direction, save, random, data) + if not bool(aside.get("ok", false)): + return aside + break var walked: Dictionary = _gen1_walk(world, cell, save, random, data) if not bool(walked.get("ok", false)): return walked - if world.map_id() != from: + if bool(walked.get("warped", false)): walked["transition"] = {"ok": true, "from_map": from, "to_map": world.map_id(), "to_cell": world.player_cell} return walked ## `CheckWarpsCollision`: a mat the landing did not take is left by pressing @@ -9565,7 +10066,7 @@ func _gen1_cross( ) -> Dictionary: var runs: Array = [] for _attempt: int in WALK_RESOLVE_ATTEMPTS: - var walked: Dictionary = _walk_to_connection(world, direction, 0, map) + var walked: Dictionary = _walk_to_connection(world, direction, 0, map, _gen1_surfing(world)) var settled: Dictionary = _gen1_settle(world, save, random, data, walked.get("events", [])) if not settled.get("texts", []).is_empty() or not settled.get("battles", []).is_empty(): runs.append({"cell": _cell_value(world), "texts": settled.get("texts", []), @@ -9584,13 +10085,13 @@ func _gen1_cross( func _gen1_talk( world: Gen2WorldAPI, cell: Vector2i, facing: int, save: Gen2SaveData, - random: RandomNumberGenerator, data: GameData + random: RandomNumberGenerator, data: GameData, answers: Array[int] = [] ) -> Dictionary: var walked: Dictionary = _gen1_walk(world, cell, save, random, data) if not bool(walked.get("ok", false)): return walked world.player_facing = facing - var settled: Dictionary = _gen1_settle(world, save, random, data, world.interact()) + var settled: Dictionary = _gen1_settle(world, save, random, data, world.interact(), answers) settled["runs"] = walked["runs"] return settled @@ -9689,17 +10190,18 @@ func _gen1_lab_leg( world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, path: Array ) -> Dictionary: + var ball: Array = GEN1_LAB_STARTER_BALLS[data.id] var starter: Dictionary = _gen1_talk( - world, GEN1_LAB_BELOW_CHARMANDER, Gen2WorldSprite.FACING_UP, save, random, data + world, ball[0], Gen2WorldSprite.FACING_UP, save, random, data ) - var stepped: Dictionary = _gen1_step(path, "oaks_lab_charmander", world, starter, { + var stepped: Dictionary = _gen1_step(path, "oaks_lab_starter", world, starter, { "party": _party_species(save)}) if not bool(stepped["ok"]): return stepped - stepped = _gen1_flag_leg(path, "oaks_lab_charmander", world, GEN1_EVENT_GOT_STARTER, "EVENT_GOT_STARTER") + stepped = _gen1_flag_leg(path, "oaks_lab_starter", world, GEN1_EVENT_GOT_STARTER, "EVENT_GOT_STARTER") if not bool(stepped["ok"]): return stepped - if save.party.is_empty() or int(save.party[0].species) != GEN1_CHARMANDER_DEX: + if save.party.is_empty() or int(save.party[0].species) != int(ball[1]): return {"ok": false, "path": path, "reason": "the ball gave %s" % [_party_species(save)]} var leaving: Dictionary = _gen1_warp_walk(world, GEN1_LAB_DOOR, save, random, data) stepped = _gen1_step(path, "oaks_lab_rival_fight", world, leaving, {"party": _party_species(save)}) @@ -9741,11 +10243,18 @@ func _gen1_parcel_leg( ]) +## A row is [direction, map, step], with the edge cell to cross at fourth. func _gen1_crossings( path: Array, world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, legs: Array ) -> Dictionary: for leg: Array in legs: + if leg.size() > 3: + var stepped_to: Dictionary = _gen1_step( + path, "%s_edge" % leg[2], world, _gen1_walk(world, leg[3], save, random, data) + ) + if not bool(stepped_to["ok"]): + return stepped_to var crossed: Dictionary = _gen1_cross(world, String(leg[0]), int(leg[1]), save, random, data) var stepped: Dictionary = _gen1_step(path, String(leg[2]), world, crossed) if not bool(stepped["ok"]): @@ -9901,7 +10410,9 @@ func _gen1_mt_moon_leg( "items": _named_items(data, world.state.items())}) if not bool(stepped["ok"]): return stepped - stepped = _gen1_flag_leg(path, "mt_moon_dome_fossil", world, GEN1_EVENT_GOT_DOME_FOSSIL, "EVENT_GOT_DOME_FOSSIL") + stepped = _gen1_flag_leg( + path, "mt_moon_dome_fossil", world, GEN1_EVENT_GOT_DOME_FOSSIL[data.id], "EVENT_GOT_DOME_FOSSIL" + ) if not bool(stepped["ok"]): return stepped return _gen1_warp_legs(path, world, save, random, data, [ @@ -9928,6 +10439,21 @@ func _gen1_bills_house_leg( ) -> Dictionary: var stepped: Dictionary = _gen1_warp_legs(path, world, save, random, data, [ ["north", GEN1_ROUTE_24, "cerulean_to_route_24"], + ]) + if not bool(stepped["ok"]): + return stepped + ## Yellow's PIKACHU learns no HM01; Damian's CHARMANDER cuts the gym's tree. + if data.id == RomRegistry.YELLOW: + var damian: Dictionary = _gen1_talk( + world, GEN1_BELOW_DAMIAN, Gen2WorldSprite.FACING_UP, save, random, data + ) + stepped = _gen1_step(path, "route_24_damian", world, damian, {"party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + if _party_species(save).size() != 2: + return {"ok": false, "path": path, "reason": "route_24_damian: Damian left the party %s" % [ + _party_species(save)]} + stepped = _gen1_warp_legs(path, world, save, random, data, [ ["east", GEN1_ROUTE_25, "route_24_to_route_25"], [GEN1_BILLS_HOUSE, "route_25_to_bills_house"], ]) @@ -10069,16 +10595,9 @@ func _gen1_vermilion_gym_leg( "moves": _party_moves(save)}) if not bool(stepped["ok"]): return stepped - var walked: Dictionary = _gen1_walk(world, GEN1_VERMILION_TREE_APPROACH, save, random, data) - if bool(walked.get("ok", false)): - world.player_facing = Gen2WorldSprite.FACING_RIGHT - var request: Dictionary = world.cut_request() - if not bool(request.get("ok", false)): - walked = {"ok": false, "reason": "cut refused: %s" % request.get("reason", "")} - else: - var applied: Dictionary = world.complete_cut() - walked = {"ok": bool(applied.get("ok", false)), "reason": applied.get("reason", "")} - stepped = _gen1_step(path, "vermilion_cut_tree", world, walked) + stepped = _gen1_step(path, "vermilion_cut_tree", world, _gen1_cut( + world, GEN1_VERMILION_TREE_APPROACH, Gen2WorldSprite.FACING_RIGHT, save, random, data + )) if not bool(stepped["ok"]): return stepped stepped = _gen1_warp_legs(path, world, save, random, data, [ @@ -10123,3 +10642,1112 @@ func _gen1_vermilion_gym_leg( if not world.state.is_engine_flag_active(Gen2WorldState.gen1_badge_flag(GEN1_BIT_THUNDERBADGE)): return {"ok": false, "path": path, "reason": "vermilion_gym_lt_surge: THUNDERBADGE is clear"} return _gen1_warp_legs(path, world, save, random, data, [[GEN1_VERMILION_CITY, "vermilion_gym_exit"]]) + + +## `UsedCut` on the tree in front of [param cell], faced [param facing]. +func _gen1_cut( + world: Gen2WorldAPI, cell: Vector2i, facing: int, save: Gen2SaveData, + random: RandomNumberGenerator, data: GameData +) -> Dictionary: + var walked: Dictionary = _gen1_walk(world, cell, save, random, data) + if not bool(walked.get("ok", false)): + return walked + world.player_facing = facing + var request: Dictionary = world.cut_request() + if not bool(request.get("ok", false)): + return {"ok": false, "reason": "cut refused: %s" % request.get("reason", ""), "runs": walked["runs"]} + var applied: Dictionary = world.complete_cut() + return {"ok": bool(applied.get("ok", false)), "reason": applied.get("reason", ""), "runs": walked["runs"]} + + +## A surfing walk keeps to the water until the cell it was asked for, which is +## where it lands, as `.stopSurfing` has the step onto a passable tile end it. +func _gen1_surfing(world: Gen2WorldAPI) -> bool: + return world.movement_mode == Gen2WorldAPI.MOVEMENT_SURF + + +## `ItemUseSurfboard` from [param cell] facing [param facing]'s water. +func _gen1_surf( + world: Gen2WorldAPI, cell: Vector2i, facing: int, save: Gen2SaveData, + random: RandomNumberGenerator, data: GameData +) -> Dictionary: + var walked: Dictionary = _gen1_walk(world, cell, save, random, data) + if not bool(walked.get("ok", false)): + return walked + world.player_facing = facing + var request: Dictionary = world.surf_request() + if not bool(request.get("ok", false)): + return {"ok": false, "reason": "surf refused: %s" % request.get("reason", ""), "runs": walked["runs"]} + var applied: Dictionary = world.complete_surf() + if not bool(applied.get("ok", false)): + return {"ok": false, "reason": "surf failed: %s" % applied.get("reason", ""), "runs": walked["runs"]} + var settled: Dictionary = _gen1_settle(world, save, random, data) + settled["runs"] = walked["runs"] + return settled + + +## Vermilion back up the Underground Path, Route 9 behind its tree, Rock +## Tunnel's two ladders and out to Lavender Town. +func _gen1_rock_tunnel_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_step(path, "vermilion_cut_tree_again", world, _gen1_cut( + world, GEN1_VERMILION_TREE_INSIDE, Gen2WorldSprite.FACING_UP, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + ["north", GEN1_ROUTE_6, "vermilion_to_route_6"], + [GEN1_UNDERGROUND_PATH_ROUTE_6, "route_6_to_underground"], + [GEN1_UNDERGROUND_PATH_NORTH_SOUTH, "underground_south_entrance"], + [GEN1_UNDERGROUND_PATH_ROUTE_5, "underground_north_exit"], + [GEN1_ROUTE_5, "underground_to_route_5"], + ["north", GEN1_CERULEAN_CITY, "route_5_to_cerulean"], + ["east", GEN1_ROUTE_9, "cerulean_to_route_9"], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_step(path, "route_9_cut_tree", world, _gen1_cut( + world, GEN1_ROUTE_9_TREE_APPROACH, Gen2WorldSprite.FACING_RIGHT, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + var legs: Array = [ + ["east", GEN1_ROUTE_10, "route_9_to_route_10"], + [GEN1_ROCK_TUNNEL_1F, "route_10_to_rock_tunnel"], + ] + for ladder: int in GEN1_ROCK_TUNNEL_LADDERS.size(): + var row: Array = GEN1_ROCK_TUNNEL_LADDERS[ladder] + legs.append([int(row[0]), "rock_tunnel_ladder_%d" % (ladder + 1), row[1]]) + legs.append_array([ + [GEN1_ROUTE_10, "rock_tunnel_to_route_10", GEN1_ROCK_TUNNEL_SOUTH_EXIT], + ["south", GEN1_LAVENDER_TOWN, "route_10_to_lavender"], + ]) + return _gen1_warp_legs(path, world, save, random, data, legs) + + +## Route 8's Underground Path to Celadon City, the gym's own tree and Erika. +func _gen1_celadon_gym_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_warp_legs(path, world, save, random, data, [ + ["west", GEN1_ROUTE_8, "lavender_to_route_8"], + [GEN1_UNDERGROUND_PATH_ROUTE_8, "route_8_to_underground", GEN1_ROUTE_8_UNDERGROUND], + [GEN1_UNDERGROUND_PATH_WEST_EAST, "underground_east_entrance"], + [GEN1_UNDERGROUND_PATH_ROUTE_7, "underground_west_exit"], + [GEN1_ROUTE_7, "underground_to_route_7"], + ["west", GEN1_CELADON_CITY, "route_7_to_celadon"], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_step(path, "celadon_cut_tree", world, _gen1_cut( + world, GEN1_CELADON_TREE_APPROACH, Gen2WorldSprite.FACING_DOWN, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_CELADON_GYM, "celadon_gym_entry", GEN1_CELADON_GYM_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_step(path, "celadon_gym_cut_tree", world, _gen1_cut( + world, GEN1_CELADON_GYM_TREE_APPROACH, Gen2WorldSprite.FACING_UP, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + var erika: Dictionary = _gen1_talk_to(world, GEN1_ERIKA, save, random, data) + stepped = _gen1_step(path, "celadon_gym_erika", world, erika, { + "party": _party_species(save), "items": _named_items(data, world.state.items()), + "badges": world.state.badge_count(), + }) + if not bool(stepped["ok"]): + return stepped + for flag: Array in [ + [GEN1_EVENT_BEAT_ERIKA, "EVENT_BEAT_ERIKA"], [GEN1_EVENT_GOT_TM21, "EVENT_GOT_TM21"], + ]: + stepped = _gen1_flag_leg(path, "celadon_gym_erika", world, int(flag[0]), String(flag[1])) + if not bool(stepped["ok"]): + return stepped + if not world.state.is_engine_flag_active(Gen2WorldState.gen1_badge_flag(GEN1_BIT_RAINBOWBADGE)): + return {"ok": false, "path": path, "reason": "celadon_gym_erika: RAINBOWBADGE is clear"} + return _gen1_warp_legs(path, world, save, random, data, [[GEN1_CELADON_CITY, "celadon_gym_exit"]]) + + +## The Game Corner's poster and the hideout's four floors down to the LIFT KEY. +func _gen1_rocket_hideout_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_step(path, "celadon_cut_tree_again", world, _gen1_cut( + world, GEN1_CELADON_TREE_INSIDE, Gen2WorldSprite.FACING_UP, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_GAME_CORNER, "game_corner_entry", GEN1_GAME_CORNER_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + var guard: Dictionary = _gen1_talk_to(world, GEN1_GAME_CORNER_ROCKET, save, random, data) + stepped = _gen1_step(path, "game_corner_rocket", world, guard, {"party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + var poster: Dictionary = _gen1_talk( + world, GEN1_GAME_CORNER_ROCKET, Gen2WorldSprite.FACING_UP, save, random, data + ) + stepped = _gen1_step(path, "game_corner_poster", world, poster) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg( + path, "game_corner_poster", world, GEN1_EVENT_FOUND_ROCKET_HIDEOUT, "EVENT_FOUND_ROCKET_HIDEOUT" + ) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_ROCKET_HIDEOUT_B1F, "game_corner_stairs", GEN1_GAME_CORNER_STAIRS], + [GEN1_ROCKET_HIDEOUT_B2F, "hideout_b1f_to_b2f", GEN1_HIDEOUT_B1F_STAIRS], + [GEN1_ROCKET_HIDEOUT_B3F, "hideout_b2f_to_b3f", GEN1_HIDEOUT_B2F_STAIRS], + [GEN1_ROCKET_HIDEOUT_B4F, "hideout_b3f_to_b4f", GEN1_HIDEOUT_B3F_STAIRS], + ]) + if not bool(stepped["ok"]): + return stepped + var rocket: Dictionary = _gen1_talk_to(world, GEN1_HIDEOUT_ROCKET_3, save, random, data) + stepped = _gen1_step(path, "hideout_b4f_rocket_3", world, rocket, {"party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg( + path, "hideout_b4f_rocket_3", world, GEN1_EVENT_ROCKET_DROPPED_LIFT_KEY, "EVENT_ROCKET_DROPPED_LIFT_KEY" + ) + if not bool(stepped["ok"]): + return stepped + var key: Dictionary = _gen1_talk(world, GEN1_BELOW_LIFT_KEY, Gen2WorldSprite.FACING_UP, save, random, data) + stepped = _gen1_step(path, "hideout_lift_key", world, key, {"items": _named_items(data, world.state.items())}) + if not bool(stepped["ok"]): + return stepped + if int(world.state.items().get(GEN1_LIFT_KEY, 0)) != 1: + return {"ok": false, "path": path, "reason": "hideout_lift_key: the bag holds %s" % [ + _named_items(data, world.state.items())]} + return _gen1_giovanni_leg(world, save, random, data, path) + + +## The car up to Giovanni and the SILPH SCOPE he leaves behind. +func _gen1_giovanni_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_ROCKET_HIDEOUT_B3F, "hideout_b4f_to_b3f", GEN1_HIDEOUT_B4F_STAIRS], + [GEN1_ROCKET_HIDEOUT_B2F, "hideout_b3f_to_b2f", GEN1_HIDEOUT_B3F_UP], + [GEN1_ROCKET_HIDEOUT_ELEVATOR, "hideout_b2f_elevator", GEN1_HIDEOUT_B2F_ELEVATOR], + ]) + if not bool(stepped["ok"]): + return stepped + _gen1_elevator_floor = GEN1_ROCKET_HIDEOUT_B4F + var panel: Dictionary = _gen1_talk(world, GEN1_HIDEOUT_CAR_PANEL, Gen2WorldSprite.FACING_UP, save, random, data) + stepped = _gen1_step(path, "hideout_elevator_panel", world, panel) + if not bool(stepped["ok"]): + return stepped + if _gen1_elevator_floor >= 0: + return {"ok": false, "path": path, "reason": "hideout_elevator_panel: the car offered no floor"} + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_ROCKET_HIDEOUT_B4F, "hideout_elevator_to_b4f", GEN1_HIDEOUT_CAR_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + for rocket_cell: Vector2i in GEN1_HIDEOUT_DOOR_ROCKETS: + var fought: Dictionary = _gen1_talk_to(world, rocket_cell, save, random, data) + stepped = _gen1_step(path, "hideout_door_rocket_%d" % rocket_cell.x, world, fought, { + "party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + var giovanni: Dictionary = _gen1_talk_to(world, GEN1_HIDEOUT_GIOVANNI, save, random, data) + stepped = _gen1_step(path, "hideout_giovanni", world, giovanni, {"party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg( + path, "hideout_giovanni", world, GEN1_EVENT_BEAT_ROCKET_HIDEOUT_GIOVANNI, + "EVENT_BEAT_ROCKET_HIDEOUT_GIOVANNI" + ) + if not bool(stepped["ok"]): + return stepped + var scope: Dictionary = _gen1_talk( + world, GEN1_BELOW_SILPH_SCOPE, Gen2WorldSprite.FACING_UP, save, random, data + ) + stepped = _gen1_step(path, "hideout_silph_scope", world, scope, { + "items": _named_items(data, world.state.items())}) + if not bool(stepped["ok"]): + return stepped + if int(world.state.items().get(GEN1_SILPH_SCOPE, 0)) != 1: + return {"ok": false, "path": path, "reason": "hideout_silph_scope: the bag holds %s" % [ + _named_items(data, world.state.items())]} + return {"ok": true} + + +## The car back up, the tower floor by floor, and Mr. Fuji's POKé FLUTE. +func _gen1_pokemon_tower_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_ROCKET_HIDEOUT_ELEVATOR, "hideout_b4f_elevator", GEN1_HIDEOUT_B4F_ELEVATOR], + ]) + if not bool(stepped["ok"]): + return stepped + _gen1_elevator_floor = GEN1_ROCKET_HIDEOUT_B1F + var panel: Dictionary = _gen1_talk(world, GEN1_HIDEOUT_CAR_PANEL, Gen2WorldSprite.FACING_UP, save, random, data) + stepped = _gen1_step(path, "hideout_elevator_panel_up", world, panel) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_ROCKET_HIDEOUT_B1F, "hideout_elevator_to_b1f", GEN1_HIDEOUT_CAR_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + var door_rocket: Dictionary = _gen1_talk_to(world, GEN1_HIDEOUT_B1F_DOOR_ROCKET, save, random, data) + stepped = _gen1_step(path, "hideout_b1f_door_rocket", world, door_rocket, {"party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_GAME_CORNER, "hideout_b1f_to_game_corner", GEN1_HIDEOUT_B1F_ELEVATOR_STAIRS], + [GEN1_CELADON_CITY, "game_corner_exit"], + ["east", GEN1_ROUTE_7, "celadon_to_route_7"], + [GEN1_UNDERGROUND_PATH_ROUTE_7, "route_7_to_underground"], + [GEN1_UNDERGROUND_PATH_WEST_EAST, "underground_west_entrance"], + [GEN1_UNDERGROUND_PATH_ROUTE_8, "underground_east_exit"], + [GEN1_ROUTE_8, "underground_to_route_8"], + ["east", GEN1_LAVENDER_TOWN, "route_8_to_lavender"], + [GEN1_POKEMON_TOWER_1F, "lavender_to_tower", GEN1_LAVENDER_TOWER_DOOR], + [GEN1_POKEMON_TOWER_2F, "tower_1f_to_2f", GEN1_TOWER_EAST_STAIRS], + [GEN1_POKEMON_TOWER_3F, "tower_2f_to_3f", GEN1_TOWER_WEST_STAIRS], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg( + path, "tower_2f_to_3f", world, GEN1_EVENT_BEAT_POKEMON_TOWER_RIVAL, "EVENT_BEAT_POKEMON_TOWER_RIVAL" + ) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_POKEMON_TOWER_4F, "tower_3f_to_4f", GEN1_TOWER_EAST_STAIRS], + [GEN1_POKEMON_TOWER_5F, "tower_4f_to_5f", GEN1_TOWER_WEST_STAIRS], + [GEN1_POKEMON_TOWER_6F, "tower_5f_to_6f", GEN1_TOWER_EAST_STAIRS], + ]) + if not bool(stepped["ok"]): + return stepped + var candy: Dictionary = _gen1_talk(world, GEN1_ABOVE_RARE_CANDY, Gen2WorldSprite.FACING_DOWN, save, random, data) + stepped = _gen1_step(path, "tower_6f_rare_candy", world, candy, {"items": _named_items(data, world.state.items())}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_POKEMON_TOWER_7F, "tower_6f_marowak_to_7f", GEN1_TOWER_7F_STAIRS], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "tower_6f_marowak_to_7f", world, GEN1_EVENT_BEAT_GHOST_MAROWAK, "EVENT_BEAT_GHOST_MAROWAK") + if not bool(stepped["ok"]): + return stepped + var fuji: Dictionary = _gen1_talk_to(world, GEN1_MR_FUJI, save, random, data) + stepped = _gen1_step(path, "tower_7f_mr_fuji", world, fuji, {"party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + if world.map_id() != Vector2i(0, GEN1_MR_FUJIS_HOUSE): + return {"ok": false, "path": path, "reason": "tower_7f_mr_fuji: Fuji left the player on %s" % [_map_value(world)]} + var flute: Dictionary = _gen1_talk(world, GEN1_BELOW_MR_FUJI_HOME, Gen2WorldSprite.FACING_UP, save, random, data) + stepped = _gen1_step(path, "mr_fujis_house_flute", world, flute, {"items": _named_items(data, world.state.items())}) + if not bool(stepped["ok"]): + return stepped + for flag: Array in [ + [GEN1_EVENT_RESCUED_MR_FUJI, "EVENT_RESCUED_MR_FUJI"], [GEN1_EVENT_GOT_POKE_FLUTE, "EVENT_GOT_POKE_FLUTE"], + ]: + stepped = _gen1_flag_leg(path, "mr_fujis_house_flute", world, int(flag[0]), String(flag[1])) + if not bool(stepped["ok"]): + return stepped + if int(world.state.items().get(GEN1_POKE_FLUTE, 0)) != 1: + return {"ok": false, "path": path, "reason": "mr_fujis_house_flute: the bag holds %s" % [ + _named_items(data, world.state.items())]} + return {"ok": true} + + +func _gen1_fuchsia_gym_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_LAVENDER_TOWN, "mr_fujis_house_exit"], + ["south", GEN1_ROUTE_12, "lavender_to_route_12"], + [GEN1_ROUTE_12_GATE_1F, "route_12_gate", GEN1_ROUTE_12_GATE_DOOR], + [GEN1_ROUTE_12, "route_12_gate_south", GEN1_ROUTE_12_GATE_SOUTH], + ]) + if not bool(stepped["ok"]): + return stepped + var beside: Dictionary = _gen1_walk(world, GEN1_ABOVE_SNORLAX, save, random, data) + if bool(beside.get("ok", false)): + var flute: Dictionary = world.poke_flute_request() + if not bool(flute.get("woke", false)): + beside = {"ok": false, "reason": "the flute woke nothing: %s" % [flute], "runs": beside["runs"]} + else: + beside = _gen1_settle(world, save, random, data, world.dispatch_sight_events()) + stepped = _gen1_step(path, "route_12_snorlax", world, beside, {"party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "route_12_snorlax", world, GEN1_EVENT_BEAT_ROUTE12_SNORLAX, "EVENT_BEAT_ROUTE12_SNORLAX") + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + ["south", GEN1_ROUTE_13, "route_12_to_route_13"], + ["west", GEN1_ROUTE_14, "route_13_to_route_14", GEN1_ROUTE_13_WEST_EDGE], + ["west", GEN1_ROUTE_15, "route_14_to_route_15"], + [GEN1_ROUTE_15_GATE_1F, "route_15_gate", GEN1_ROUTE_15_GATE_DOOR], + [GEN1_ROUTE_15, "route_15_gate_west", GEN1_ROUTE_15_GATE_WEST], + ["west", GEN1_FUCHSIA_CITY, "route_15_to_fuchsia"], + [GEN1_FUCHSIA_GYM, "fuchsia_gym_entry", GEN1_FUCHSIA_GYM_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + var koga: Dictionary = _gen1_talk_to(world, GEN1_KOGA, save, random, data) + stepped = _gen1_step(path, "fuchsia_gym_koga", world, koga, { + "party": _party_species(save), "items": _named_items(data, world.state.items()), + "badges": world.state.badge_count(), + }) + if not bool(stepped["ok"]): + return stepped + for flag: Array in [ + [GEN1_EVENT_BEAT_KOGA, "EVENT_BEAT_KOGA"], [GEN1_EVENT_GOT_TM06, "EVENT_GOT_TM06"], + ]: + stepped = _gen1_flag_leg(path, "fuchsia_gym_koga", world, int(flag[0]), String(flag[1])) + if not bool(stepped["ok"]): + return stepped + if not world.state.is_engine_flag_active(Gen2WorldState.gen1_badge_flag(GEN1_BIT_SOULBADGE)): + return {"ok": false, "path": path, "reason": "fuchsia_gym_koga: SOULBADGE is clear"} + return _gen1_warp_legs(path, world, save, random, data, [[GEN1_FUCHSIA_CITY, "fuchsia_gym_exit"]]) + + +func _gen1_safari_zone_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_SAFARI_ZONE_GATE, "safari_gate_entry", GEN1_SAFARI_GATE_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + var offer: Dictionary = _gen1_walk(world, GEN1_SAFARI_GATE_OFFER, save, random, data) + stepped = _gen1_step(path, "safari_gate_fee", world, offer, { + "balls": world.state.safari_balls(), "steps": world.state.safari_steps()}) + if not bool(stepped["ok"]): + return stepped + ## `SafariZoneEntranceAutoWalk` has already walked the player into the centre. + if not world.gen1_safari_active() or world.map_id() != Vector2i(0, GEN1_SAFARI_ZONE_CENTER): + return {"ok": false, "path": path, "reason": "safari_gate_fee: the fee left the player on %s" % [ + _map_value(world)]} + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_SAFARI_ZONE_EAST, "safari_center_to_east", GEN1_SAFARI_CENTER_EAST], + [GEN1_SAFARI_ZONE_NORTH, "safari_east_to_north", GEN1_SAFARI_EAST_NORTH], + [GEN1_SAFARI_ZONE_WEST, "safari_north_to_west", GEN1_SAFARI_NORTH_WEST], + [GEN1_SAFARI_ZONE_SECRET_HOUSE, "safari_secret_house", GEN1_SAFARI_SECRET_HOUSE_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + var guru: Dictionary = _gen1_talk(world, GEN1_BELOW_FISHING_GURU, Gen2WorldSprite.FACING_UP, save, random, data) + stepped = _gen1_step(path, "safari_secret_house_hm03", world, guru, { + "items": _named_items(data, world.state.items()), "steps": world.state.safari_steps()}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "safari_secret_house_hm03", world, GEN1_EVENT_GOT_HM03, "EVENT_GOT_HM03") + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_SAFARI_ZONE_WEST, "safari_secret_house_exit"], + ]) + if not bool(stepped["ok"]): + return stepped + var teeth: Dictionary = _gen1_talk(world, GEN1_BELOW_GOLD_TEETH, Gen2WorldSprite.FACING_UP, save, random, data) + stepped = _gen1_step(path, "safari_gold_teeth", world, teeth, { + "items": _named_items(data, world.state.items()), "steps": world.state.safari_steps()}) + if not bool(stepped["ok"]): + return stepped + if int(world.state.items().get(GEN1_GOLD_TEETH, 0)) != 1 or int(world.state.items().get(GEN1_HM03, 0)) != 1: + return {"ok": false, "path": path, "reason": "safari_gold_teeth: the bag holds %s" % [ + _named_items(data, world.state.items())]} + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_SAFARI_ZONE_NORTH, "safari_west_to_north", GEN1_SAFARI_WEST_NORTH], + [GEN1_SAFARI_ZONE_EAST, "safari_north_to_east", GEN1_SAFARI_NORTH_EAST], + [GEN1_SAFARI_ZONE_CENTER, "safari_east_to_center", GEN1_SAFARI_EAST_CENTER], + [GEN1_SAFARI_ZONE_GATE, "safari_center_to_gate", GEN1_SAFARI_CENTER_GATE], + [GEN1_FUCHSIA_CITY, "safari_gate_exit", GEN1_SAFARI_GATE_SOUTH], + [GEN1_WARDENS_HOUSE, "wardens_house_entry", GEN1_WARDENS_HOUSE_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + var warden: Dictionary = _gen1_talk_to(world, GEN1_WARDEN, save, random, data) + stepped = _gen1_step(path, "wardens_house_hm04", world, warden, { + "items": _named_items(data, world.state.items())}) + if not bool(stepped["ok"]): + return stepped + for flag: Array in [ + [GEN1_EVENT_GAVE_GOLD_TEETH, "EVENT_GAVE_GOLD_TEETH"], [GEN1_EVENT_GOT_HM04, "EVENT_GOT_HM04"], + ]: + stepped = _gen1_flag_leg(path, "wardens_house_hm04", world, int(flag[0]), String(flag[1])) + if not bool(stepped["ok"]): + return stepped + if int(world.state.items().get(GEN1_HM04, 0)) != 1: + return {"ok": false, "path": path, "reason": "wardens_house_hm04: the bag holds %s" % [ + _named_items(data, world.state.items())]} + return _gen1_warp_legs(path, world, save, random, data, [[GEN1_FUCHSIA_CITY, "wardens_house_exit"]]) + + +## Back to Celadon's roof for a FRESH WATER and through Route 7's gate on it. +func _gen1_saffron_drink_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_warp_legs(path, world, save, random, data, [ + ["east", GEN1_ROUTE_15, "fuchsia_to_route_15"], + [GEN1_ROUTE_15_GATE_1F, "route_15_gate_back", GEN1_ROUTE_15_WEST_DOOR], + [GEN1_ROUTE_15, "route_15_gate_east", GEN1_ROUTE_15_GATE_EAST_EXIT], + ["east", GEN1_ROUTE_14, "route_15_to_route_14"], + ["east", GEN1_ROUTE_13, "route_14_to_route_13", GEN1_ROUTE_14_EAST_EDGE], + ["north", GEN1_ROUTE_12, "route_13_to_route_12"], + [GEN1_ROUTE_12_GATE_1F, "route_12_gate_back", GEN1_ROUTE_12_GATE_SOUTH_DOOR], + [GEN1_ROUTE_12, "route_12_gate_north", GEN1_ROUTE_12_GATE_NORTH], + ["north", GEN1_LAVENDER_TOWN, "route_12_to_lavender"], + ["west", GEN1_ROUTE_8, "lavender_to_route_8_again"], + [GEN1_UNDERGROUND_PATH_ROUTE_8, "route_8_to_underground_again", GEN1_ROUTE_8_UNDERGROUND], + [GEN1_UNDERGROUND_PATH_WEST_EAST, "underground_east_entrance_again"], + [GEN1_UNDERGROUND_PATH_ROUTE_7, "underground_west_exit_again"], + [GEN1_ROUTE_7, "underground_to_route_7_again"], + ["west", GEN1_CELADON_CITY, "route_7_to_celadon_again"], + [GEN1_CELADON_MART_1F, "celadon_mart_entry", GEN1_CELADON_MART_DOOR], + [GEN1_CELADON_MART_ELEVATOR, "celadon_mart_elevator", GEN1_MART_ELEVATOR_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_ride(path, world, save, random, data, "celadon_mart_to_5f", + GEN1_MART_CAR_PANEL, GEN1_MART_CAR_DOOR, GEN1_CELADON_MART_5F) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_CELADON_MART_ROOF, "celadon_mart_roof", GEN1_MART_5F_ROOF_STAIRS], + ]) + if not bool(stepped["ok"]): + return stepped + _gen1_drink = GEN1_FRESH_WATER + var machine: Dictionary = _gen1_talk( + world, GEN1_BELOW_VENDING_MACHINE, Gen2WorldSprite.FACING_UP, save, random, data + ) + stepped = _gen1_step(path, "celadon_mart_fresh_water", world, machine, { + "items": _named_items(data, world.state.items())}) + if not bool(stepped["ok"]): + return stepped + if int(world.state.items().get(GEN1_FRESH_WATER, 0)) != 1: + return {"ok": false, "path": path, "reason": "celadon_mart_fresh_water: the bag holds %s" % [ + _named_items(data, world.state.items())]} + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_CELADON_MART_5F, "celadon_mart_roof_down", GEN1_MART_ROOF_STAIRS], + [GEN1_CELADON_MART_ELEVATOR, "celadon_mart_elevator_down", GEN1_MART_ELEVATOR_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_ride(path, world, save, random, data, "celadon_mart_to_1f", + GEN1_MART_CAR_PANEL, GEN1_MART_CAR_DOOR, GEN1_CELADON_MART_1F) + if not bool(stepped["ok"]): + return stepped + return _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_CELADON_CITY, "celadon_mart_exit", GEN1_MART_1F_DOOR], + ["east", GEN1_ROUTE_7, "celadon_to_route_7_again"], + [GEN1_ROUTE_7_GATE, "route_7_gate", GEN1_ROUTE_7_GATE_WEST], + [GEN1_ROUTE_7, "route_7_gate_east", GEN1_ROUTE_7_GATE_EAST], + ["east", GEN1_SAFFRON_CITY, "route_7_to_saffron"], + ]) + + +## The car's panel answered with [param landing], then its door. +func _gen1_ride( + path: Array, world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, + data: GameData, step: String, panel: Vector2i, door: Vector2i, landing: int +) -> Dictionary: + _gen1_elevator_floor = landing + var pressed: Dictionary = _gen1_talk(world, panel, Gen2WorldSprite.FACING_UP, save, random, data) + var stepped: Dictionary = _gen1_step(path, "%s_panel" % step, world, pressed) + if not bool(stepped["ok"]): + return stepped + if _gen1_elevator_floor >= 0: + return {"ok": false, "path": path, "reason": "%s: the car offered no floor on map %d" % [step, landing]} + return _gen1_warp_legs(path, world, save, random, data, [[landing, step, door]]) + + +## Silph Co.: the stairs to 5F, the CARD KEY past its Rocket, the pads to +## Giovanni on 11F, the president's MASTER BALL and the car down. +func _gen1_silph_co_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_SILPH_CO_1F, "silph_co_entry", GEN1_SILPH_CO_DOOR], + [GEN1_SILPH_CO_2F, "silph_1f_to_2f", GEN1_SILPH_STAIRS_RIGHT], + [GEN1_SILPH_CO_3F, "silph_2f_to_3f", GEN1_SILPH_STAIRS_RIGHT], + [GEN1_SILPH_CO_4F, "silph_3f_to_4f", GEN1_SILPH_STAIRS_LEFT], + [GEN1_SILPH_CO_5F, "silph_4f_to_5f", GEN1_SILPH_STAIRS_RIGHT], + [GEN1_SILPH_CO_9F, "silph_5f_pad_to_9f", GEN1_SILPH_5F_PAD_TO_9F], + [GEN1_SILPH_CO_5F, "silph_9f_pad_back", GEN1_SILPH_9F_PAD], + ]) + if not bool(stepped["ok"]): + return stepped + var key: Dictionary = _gen1_talk(world, GEN1_BESIDE_CARD_KEY, Gen2WorldSprite.FACING_RIGHT, save, random, data) + stepped = _gen1_step(path, "silph_5f_card_key", world, key, {"items": _named_items(data, world.state.items())}) + if not bool(stepped["ok"]): + return stepped + if int(world.state.items().get(GEN1_CARD_KEY, 0)) != 1: + return {"ok": false, "path": path, "reason": "silph_5f_card_key: the bag holds %s" % [ + _named_items(data, world.state.items())]} + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_SILPH_CO_9F, "silph_5f_pad_to_9f_again", GEN1_SILPH_5F_PAD_TO_9F], + [GEN1_SILPH_CO_5F, "silph_9f_pad_back_again", GEN1_SILPH_9F_PAD], + [GEN1_SILPH_CO_3F, "silph_5f_pad_to_3f", GEN1_SILPH_5F_PAD_TO_3F], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_card_key(path, "silph_3f_card_key", world, GEN1_SILPH_3F_DOOR_SIDE, + Gen2WorldSprite.FACING_LEFT, save, random, data) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_SILPH_CO_7F, "silph_3f_pad_to_7f", GEN1_SILPH_3F_PAD_TO_7F], + ]) + if not bool(stepped["ok"]): + return stepped + ## `SilphCo7FSilphWorkerM1Text`: the LAPRAS that will carry the walk to Cinnabar. + var lapras: Dictionary = _gen1_talk_to(world, GEN1_SILPH_LAPRAS_WORKER, save, random, data) + _mirror_party(world, save) + stepped = _gen1_step(path, "silph_7f_lapras", world, lapras, {"party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + if not _party_species(save).any(func(mon: Dictionary) -> bool: return int(mon["species"]) == GEN1_DEX_LAPRAS): + return {"ok": false, "path": path, "reason": "silph_7f_lapras: the party is %s" % [_party_species(save)]} + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_SILPH_CO_11F, "silph_7f_pad_to_11f", GEN1_SILPH_7F_PAD_TO_11F], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "silph_7f_pad_to_11f", world, GEN1_EVENT_BEAT_SILPH_CO_RIVAL, "EVENT_BEAT_SILPH_CO_RIVAL") + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_card_key(path, "silph_11f_card_key", world, GEN1_SILPH_11F_DOOR_SIDE, + Gen2WorldSprite.FACING_UP, save, random, data) + if not bool(stepped["ok"]): + return stepped + var giovanni: Dictionary = _gen1_talk_to(world, GEN1_SILPH_GIOVANNI, save, random, data) + stepped = _gen1_step(path, "silph_11f_giovanni", world, giovanni, {"party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "silph_11f_giovanni", world, GEN1_EVENT_BEAT_SILPH_CO_GIOVANNI, "EVENT_BEAT_SILPH_CO_GIOVANNI") + if not bool(stepped["ok"]): + return stepped + var president: Dictionary = _gen1_talk_to(world, GEN1_SILPH_PRESIDENT, save, random, data) + stepped = _gen1_step(path, "silph_11f_master_ball", world, president, { + "items": _named_items(data, world.state.items())}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "silph_11f_master_ball", world, GEN1_EVENT_GOT_MASTER_BALL, "EVENT_GOT_MASTER_BALL") + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_SILPH_CO_7F, "silph_11f_pad_to_7f", GEN1_SILPH_11F_PAD_TO_7F], + [GEN1_SILPH_CO_3F, "silph_7f_pad_to_3f", GEN1_SILPH_7F_PAD_TO_3F], + [GEN1_SILPH_CO_ELEVATOR, "silph_3f_elevator", GEN1_SILPH_3F_ELEVATOR], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_ride(path, world, save, random, data, "silph_car_to_1f", + GEN1_SILPH_CAR_PANEL, GEN1_SILPH_CAR_DOOR, GEN1_SILPH_CO_1F) + if not bool(stepped["ok"]): + return stepped + return _gen1_warp_legs(path, world, save, random, data, [[GEN1_SAFFRON_CITY, "silph_co_exit"]]) + + +## `PrintCardKeyText`: the door in front of [param cell] opened with the CARD KEY. +func _gen1_card_key( + path: Array, step: String, world: Gen2WorldAPI, cell: Vector2i, facing: int, + save: Gen2SaveData, random: RandomNumberGenerator, data: GameData +) -> Dictionary: + var opened: Dictionary = _gen1_talk(world, cell, facing, save, random, data) + var stepped: Dictionary = _gen1_step(path, step, world, opened) + if not bool(stepped["ok"]): + return stepped + var door: Vector2i = world.player_cell + world.facing_direction() + if not world.can_walk_to(door): + return {"ok": false, "path": path, "reason": "%s: the door at %s stayed shut" % [step, door]} + return {"ok": true} + + +## SURF on the LAPRAS, Fuchsia by the gates again, Route 19's sea, Route 20 through +## the Seafoam Islands (B3F's pool is the way between its halves) and Cinnabar. +func _gen1_cinnabar_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var taught: Dictionary = _teach_tm_hm(world, save, GEN1_HM03) + _mirror_party(world, save) + var stepped: Dictionary = _gen1_step(path, "saffron_teach_surf", world, taught, { + "moves": _party_moves(save)}) + if not bool(stepped["ok"]): + return stepped + var legs: Array = [] + for pad: int in GEN1_SAFFRON_GYM_PADS_OUT.size(): + legs.append([GEN1_SAFFRON_GYM, "saffron_gym_pad_out_%d" % (pad + 1), GEN1_SAFFRON_GYM_PADS_OUT[pad]]) + stepped = _gen1_warp_legs(path, world, save, random, data, legs + [ + [GEN1_SAFFRON_CITY, "saffron_gym_exit"], + ["east", GEN1_ROUTE_8, "saffron_to_route_8"], + [GEN1_ROUTE_8_GATE, "route_8_gate", GEN1_ROUTE_8_GATE_WEST], + [GEN1_ROUTE_8, "route_8_gate_east", GEN1_ROUTE_8_GATE_EAST], + ["east", GEN1_LAVENDER_TOWN, "route_8_to_lavender_again"], + ["south", GEN1_ROUTE_12, "lavender_to_route_12_again"], + [GEN1_ROUTE_12_GATE_1F, "route_12_gate_again", GEN1_ROUTE_12_GATE_DOOR], + [GEN1_ROUTE_12, "route_12_gate_south_again", GEN1_ROUTE_12_GATE_SOUTH], + ["south", GEN1_ROUTE_13, "route_12_to_route_13_again"], + ["west", GEN1_ROUTE_14, "route_13_to_route_14_again", GEN1_ROUTE_13_WEST_EDGE], + ["west", GEN1_ROUTE_15, "route_14_to_route_15_again"], + [GEN1_ROUTE_15_GATE_1F, "route_15_gate_again", GEN1_ROUTE_15_GATE_DOOR], + [GEN1_ROUTE_15, "route_15_gate_west_again", GEN1_ROUTE_15_GATE_WEST], + ["west", GEN1_FUCHSIA_CITY, "route_15_to_fuchsia_again"], + ["south", GEN1_ROUTE_19, "fuchsia_to_route_19"], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_step(path, "route_19_surf", world, _gen1_surf( + world, GEN1_ROUTE_19_SHORE[data.id], Gen2WorldSprite.FACING_DOWN, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_crossings(path, world, save, random, data, [ + ["west", GEN1_ROUTE_20, "route_19_to_route_20"], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_step(path, "route_20_north_field", world, _gen1_walk( + world, GEN1_ROUTE_20_NORTH_FIELD, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_SEAFOAM_ISLANDS_1F, "seafoam_west_door", GEN1_ROUTE_20_SEAFOAM_WEST], + [GEN1_SEAFOAM_ISLANDS_B1F, "seafoam_1f_to_b1f", GEN1_SEAFOAM_1F_WEST_LADDER], + [GEN1_SEAFOAM_ISLANDS_B2F, "seafoam_b1f_to_b2f", GEN1_SEAFOAM_B1F_WEST_LADDER], + [GEN1_SEAFOAM_ISLANDS_B3F, "seafoam_b2f_to_b3f", GEN1_SEAFOAM_B2F_WEST_LADDER], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_step(path, "seafoam_b3f_surf", world, _gen1_surf( + world, GEN1_SEAFOAM_B3F_WEST_BANK, Gen2WorldSprite.FACING_DOWN, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_step(path, "seafoam_b3f_east_bank", world, _gen1_walk( + world, GEN1_SEAFOAM_B3F_SHORE, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_SEAFOAM_ISLANDS_B2F, "seafoam_b3f_to_b2f", GEN1_SEAFOAM_B2F_EAST_LADDER], + [GEN1_SEAFOAM_ISLANDS_B1F, "seafoam_b2f_to_b1f", GEN1_SEAFOAM_B1F_EAST_LADDER], + [GEN1_SEAFOAM_ISLANDS_1F, "seafoam_b1f_to_1f", GEN1_SEAFOAM_1F_EAST_LADDER], + [GEN1_ROUTE_20, "seafoam_east_door", GEN1_SEAFOAM_1F_EAST_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_step(path, "route_20_surf_again", world, _gen1_surf( + world, GEN1_ROUTE_20_EAST_BEACH, Gen2WorldSprite.FACING_DOWN, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + return _gen1_warp_legs(path, world, save, random, data, [ + ["west", GEN1_CINNABAR_ISLAND, "route_20_to_cinnabar"], + ]) + + +## `MansionScript_Switches`: the statue in front of [param cell] pressed, and +## [param on] is where EVENT_MANSION_SWITCH_ON has to stand behind it. +func _gen1_mansion_switch( + path: Array, step: String, world: Gen2WorldAPI, cell: Vector2i, on: bool, + save: Gen2SaveData, random: RandomNumberGenerator, data: GameData +) -> Dictionary: + var pressed: Dictionary = _gen1_talk(world, cell, Gen2WorldSprite.FACING_UP, save, random, data) + var stepped: Dictionary = _gen1_step(path, step, world, pressed) + if not bool(stepped["ok"]): + return stepped + if world.event_flag_active(GEN1_EVENT_MANSION_SWITCH_ON) != on: + return {"ok": false, "path": path, "reason": "%s: the switch stands %s" % [ + step, "on" if world.event_flag_active(GEN1_EVENT_MANSION_SWITCH_ON) else "off"]} + return {"ok": true} + + +## The POKéMON MANSION to the SECRET KEY: 3F's switch and its hole down to 1F's +## east half, B1F's two statues turned to open the key's room and then the way +## back to its stairs, and out by the east doors. +func _gen1_mansion_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_step(path, "cinnabar_shore", world, _gen1_walk( + world, GEN1_CINNABAR_SHORE, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_POKEMON_MANSION_1F, "mansion_entry", GEN1_MANSION_DOOR], + [GEN1_POKEMON_MANSION_2F, "mansion_1f_to_2f", GEN1_MANSION_1F_STAIRS], + [GEN1_POKEMON_MANSION_3F, "mansion_2f_to_3f", GEN1_MANSION_2F_STAIRS], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_mansion_switch(path, "mansion_3f_switch_on", world, GEN1_MANSION_3F_SWITCH, true, save, random, data) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_POKEMON_MANSION_1F, "mansion_3f_hole", GEN1_MANSION_3F_HOLE], + [GEN1_POKEMON_MANSION_B1F, "mansion_1f_to_b1f", GEN1_MANSION_1F_B1F_STAIRS], + ]) + if not bool(stepped["ok"]): + return stepped + for turn: Array in [ + ["mansion_b1f_south_switch_off", GEN1_MANSION_B1F_SOUTH_SWITCH, false], + ["mansion_b1f_north_switch_on", GEN1_MANSION_B1F_NORTH_SWITCH, true], + ]: + stepped = _gen1_mansion_switch(path, String(turn[0]), world, turn[1], bool(turn[2]), save, random, data) + if not bool(stepped["ok"]): + return stepped + var key: Dictionary = _gen1_talk_to(world, GEN1_MANSION_SECRET_KEY, save, random, data) + stepped = _gen1_step(path, "mansion_b1f_secret_key", world, key, { + "items": _named_items(data, world.state.items())}) + if not bool(stepped["ok"]): + return stepped + if int(world.state.items().get(GEN1_SECRET_KEY, 0)) != 1: + return {"ok": false, "path": path, "reason": "mansion_b1f_secret_key: the bag holds %s" % [ + _named_items(data, world.state.items())]} + for turn: Array in [ + ["mansion_b1f_north_switch_off", GEN1_MANSION_B1F_NORTH_SWITCH, false], + ["mansion_b1f_south_switch_on", GEN1_MANSION_B1F_SOUTH_SWITCH, true], + ]: + stepped = _gen1_mansion_switch(path, String(turn[0]), world, turn[1], bool(turn[2]), save, random, data) + if not bool(stepped["ok"]): + return stepped + return _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_POKEMON_MANSION_1F, "mansion_b1f_to_1f", GEN1_MANSION_B1F_STAIRS], + [GEN1_CINNABAR_ISLAND, "mansion_exit", GEN1_MANSION_EAST_DOOR], + ]) + + +## Cinnabar's gym: the six quiz machines answered as `CinnabarGymQuiz`'s own +## nibble has them, each gate opening on the way round to Blaine. +func _gen1_cinnabar_gym_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + ## Twenty items fill a Generation 1 bag, and Blaine's TM38 is the twenty-first. + var tossed: Dictionary = Gen2WorldBagHost.toss(world, save, GEN1_NUGGET, 1, false) + var stepped: Dictionary = _gen1_step(path, "cinnabar_toss_nugget", world, tossed, { + "items": _named_items(data, world.state.items())}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_CINNABAR_GYM, "cinnabar_gym_entry", GEN1_CINNABAR_GYM_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + for machine: int in GEN1_CINNABAR_QUIZ.size(): + var row: Array = GEN1_CINNABAR_QUIZ[machine] + var answer: Array[int] = [int(row[1])] + var quiz: Dictionary = _gen1_talk(world, row[0], Gen2WorldSprite.FACING_UP, save, random, data, answer) + stepped = _gen1_step(path, "cinnabar_gym_quiz_%d" % (machine + 1), world, quiz, { + "party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "cinnabar_gym_quiz_%d" % (machine + 1), world, + GEN1_EVENT_CINNABAR_GYM_GATE0_UNLOCKED + machine + 1, "EVENT_CINNABAR_GYM_GATE%d_UNLOCKED" % (machine + 1)) + if not bool(stepped["ok"]): + return stepped + var blaine: Dictionary = _gen1_talk_to(world, GEN1_BLAINE, save, random, data) + stepped = _gen1_step(path, "cinnabar_gym_blaine", world, blaine, { + "party": _party_species(save), "items": _named_items(data, world.state.items()), + "badges": world.state.badge_count(), + }) + if not bool(stepped["ok"]): + return stepped + for flag: Array in [ + [GEN1_EVENT_BEAT_BLAINE, "EVENT_BEAT_BLAINE"], [GEN1_EVENT_GOT_TM38, "EVENT_GOT_TM38"], + ]: + stepped = _gen1_flag_leg(path, "cinnabar_gym_blaine", world, int(flag[0]), String(flag[1])) + if not bool(stepped["ok"]): + return stepped + if not world.state.is_engine_flag_active(Gen2WorldState.gen1_badge_flag(GEN1_BIT_VOLCANOBADGE)): + return {"ok": false, "path": path, "reason": "cinnabar_gym_blaine: VOLCANOBADGE is clear"} + return _gen1_warp_legs(path, world, save, random, data, [[GEN1_CINNABAR_ISLAND, "cinnabar_gym_exit"]]) + + +## Route 21 north to Pallet, Route 1 to Viridian and its gym: Giovanni again, +## the EARTHBADGE and TM27. +func _gen1_viridian_gym_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_step(path, "cinnabar_surf_north", world, _gen1_surf( + world, GEN1_CINNABAR_WEST_SHORE, Gen2WorldSprite.FACING_LEFT, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_crossings(path, world, save, random, data, [ + ["north", GEN1_ROUTE_21, "cinnabar_to_route_21"], + ["north", GEN1_PALLET_TOWN, "route_21_to_pallet"], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_step(path, "pallet_shore", world, _gen1_walk( + world, GEN1_PALLET_SHORE, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + var tossed: Dictionary = Gen2WorldBagHost.toss(world, save, GEN1_TM11, 1, false) + stepped = _gen1_step(path, "pallet_toss_tm11", world, tossed, { + "items": _named_items(data, world.state.items())}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + ["north", GEN1_ROUTE_1, "pallet_to_route_1_again"], + ["north", GEN1_VIRIDIAN_CITY, "route_1_to_viridian_again"], + [GEN1_VIRIDIAN_GYM, "viridian_gym_entry", GEN1_VIRIDIAN_GYM_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + var giovanni: Dictionary = _gen1_talk_to(world, GEN1_VIRIDIAN_GIOVANNI, save, random, data) + stepped = _gen1_step(path, "viridian_gym_giovanni", world, giovanni, { + "party": _party_species(save), "items": _named_items(data, world.state.items()), + "badges": world.state.badge_count(), + }) + if not bool(stepped["ok"]): + return stepped + for flag: Array in [ + [GEN1_EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI, "EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI"], + [GEN1_EVENT_GOT_TM27, "EVENT_GOT_TM27"], + ]: + stepped = _gen1_flag_leg(path, "viridian_gym_giovanni", world, int(flag[0]), String(flag[1])) + if not bool(stepped["ok"]): + return stepped + if not world.state.is_engine_flag_active(Gen2WorldState.gen1_badge_flag(GEN1_BIT_EARTHBADGE)): + return {"ok": false, "path": path, "reason": "viridian_gym_giovanni: EARTHBADGE is clear"} + return _gen1_warp_legs(path, world, save, random, data, [[GEN1_VIRIDIAN_CITY, "viridian_gym_exit"]]) + + +## `TryPushingBoulder` [param count] times from [param approach], the player +## stepping after the boulder between pushes, since `MoveSprite` moves it alone. +func _gen1_push( + path: Array, step: String, world: Gen2WorldAPI, approach: Vector2i, direction: Vector2i, + count: int, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData +) -> Dictionary: + var walked: Dictionary = _gen1_walk(world, approach, save, random, data) + var stepped: Dictionary = _gen1_step(path, "%s_approach" % step, world, walked) + if not bool(stepped["ok"]): + return stepped + var pushes: Array = [] + for _push: int in count: + _settle_object_steps(world, random) + ## The first bump only arms `TryPushingBoulder`; the next press pushes. + var result: Dictionary = world.move_result(direction) + if not result.has("boulder_pushed"): + result = world.move_result(direction) + if not result.has("boulder_pushed"): + return _gen1_step(path, step, world, {"ok": false, "reason": "no boulder moved from %s: %s" % [ + _cell_value(world), result.get("reason", "the step succeeded")]}) + var settled: Dictionary = _gen1_settle(world, save, random, data, _dispatch_after_step(world)) + if not bool(settled.get("ok", false)): + return _gen1_step(path, step, world, settled) + pushes.append(_cell_value_from_vector((result["boulder_pushed"] as Dictionary)["to_cell"])) + _settle_object_steps(world, random) + if _push + 1 < count: + var followed: Dictionary = _gen1_walk(world, world.player_cell + direction, save, random, data) + if not bool(followed.get("ok", false)): + return _gen1_step(path, step, world, followed) + return _gen1_step(path, step, world, {"ok": true}, {"boulder": pushes}) + + +## STRENGTH on the LAPRAS, Route 22, its gate, and Route 23's guards and river +## to Victory Road's door. +func _gen1_route_23_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var taught: Dictionary = _teach_tm_hm(world, save, GEN1_HM04) + _mirror_party(world, save) + var stepped: Dictionary = _gen1_step(path, "viridian_teach_strength", world, taught, { + "moves": _party_moves(save)}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + ["west", GEN1_ROUTE_22, "viridian_to_route_22"], + [GEN1_ROUTE_22_GATE, "route_22_gate", GEN1_ROUTE_22_GATE_DOOR], + [GEN1_ROUTE_23, "route_22_gate_north", GEN1_ROUTE_22_GATE_NORTH], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_step(path, "route_23_surf", world, _gen1_surf( + world, GEN1_ROUTE_23_SOUTH_BANK, Gen2WorldSprite.FACING_UP, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_step(path, "route_23_north_bank", world, _gen1_walk( + world, GEN1_ROUTE_23_NORTH_BANK, save, random, data + )) + if not bool(stepped["ok"]): + return stepped + return _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_VICTORY_ROAD_1F, "victory_road_entry", GEN1_ROUTE_23_VICTORY_ROAD_DOOR], + ]) + + +## Victory Road's three switches: 1F's boulder to the ladder, 2F's first to the +## east, 3F's into its hole and down onto 2F's second, then out to the Plateau. +func _gen1_victory_road_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var strength: Dictionary = world.strength_request() + if bool(strength.get("ok", false)): + strength = world.complete_strength() + var stepped: Dictionary = _gen1_step(path, "victory_road_strength", world, strength) + if not bool(stepped["ok"]): + return stepped + for push: Array in GEN1_VICTORY_ROAD_1F_PUSHES: + stepped = _gen1_push(path, String(push[0]), world, push[1], push[2], int(push[3]), save, random, data) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "victory_road_1f_switch", world, + GEN1_EVENT_VICTORY_ROAD_1_BOULDER_ON_SWITCH, "EVENT_VICTORY_ROAD_1_BOULDER_ON_SWITCH") + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_VICTORY_ROAD_2F, "victory_road_1f_to_2f", GEN1_VICTORY_ROAD_1F_LADDER], + ]) + if not bool(stepped["ok"]): + return stepped + for push: Array in GEN1_VICTORY_ROAD_2F_PUSHES: + stepped = _gen1_push(path, String(push[0]), world, push[1], push[2], int(push[3]), save, random, data) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "victory_road_2f_switch", world, + GEN1_EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH1, "EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH1") + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_VICTORY_ROAD_3F, "victory_road_2f_to_3f", GEN1_VICTORY_ROAD_2F_LADDER], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_push(path, "victory_road_3f_push_gap", world, GEN1_VICTORY_ROAD_3F_GAP_ABOVE, + Vector2i.DOWN, 2, save, random, data) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_push(path, "victory_road_3f_push_into_hole", world, GEN1_VICTORY_ROAD_3F_HOLE_SIDE, + Vector2i.RIGHT, 1, save, random, data) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_VICTORY_ROAD_2F, "victory_road_3f_hole", GEN1_VICTORY_ROAD_3F_HOLE], + ]) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_push(path, "victory_road_2f_push_onto_second_switch", world, + GEN1_VICTORY_ROAD_2F_FALLEN_BOULDER_SIDE, Vector2i.LEFT, GEN1_VICTORY_ROAD_2F_SECOND_PUSHES, + save, random, data) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "victory_road_2f_second_switch", world, + GEN1_EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH2, "EVENT_VICTORY_ROAD_2_BOULDER_ON_SWITCH2") + if not bool(stepped["ok"]): + return stepped + return _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_VICTORY_ROAD_3F, "victory_road_2f_to_3f_again", GEN1_VICTORY_ROAD_2F_EAST_LADDER], + [GEN1_VICTORY_ROAD_2F, "victory_road_3f_to_2f", GEN1_VICTORY_ROAD_3F_LADDER_DOWN], + [GEN1_ROUTE_23, "victory_road_exit", GEN1_VICTORY_ROAD_2F_EXIT], + ["north", GEN1_INDIGO_PLATEAU, "route_23_to_indigo_plateau"], + [GEN1_INDIGO_PLATEAU_LOBBY, "indigo_plateau_lobby", GEN1_INDIGO_PLATEAU_DOOR], + ]) + + +## The Elite Four's four rooms, the Champion and the Hall of Fame. +func _gen1_elite_four_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var stepped: Dictionary = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_LORELEIS_ROOM, "loreleis_room", GEN1_LOBBY_STAIRS], + ]) + if not bool(stepped["ok"]): + return stepped + for room: Array in GEN1_ELITE_FOUR: + var member: Dictionary = _gen1_talk_to(world, GEN1_ELITE_MEMBER, save, random, data) + stepped = _gen1_step(path, "%s_fought" % room[0], world, member, {"party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "%s_fought" % room[0], world, int(room[2]), String(room[3])) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [int(room[1]), "%s_next_door" % room[0], GEN1_ELITE_NEXT_DOOR], + ]) + if not bool(stepped["ok"]): + return stepped + var lance: Dictionary = _gen1_talk_to(world, GEN1_LANCE, save, random, data) + stepped = _gen1_step(path, "lance_fought", world, lance, {"party": _party_species(save)}) + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_flag_leg(path, "lance_fought", world, GEN1_EVENT_BEAT_LANCE, "EVENT_BEAT_LANCE") + if not bool(stepped["ok"]): + return stepped + stepped = _gen1_warp_legs(path, world, save, random, data, [ + [GEN1_CHAMPIONS_ROOM, "champions_room", GEN1_LANCES_ROOM_EXIT, GEN1_HALL_OF_FAME], + ]) + if not bool(stepped["ok"]): + return stepped + ## `ChampionsRoomOakAppearsScript` walks the player into the Hall, whose own + ## script inducts the party, clears every Plateau event, saves and resets, + ## all inside the one drain. + path.append({ + "step": "hall_of_fame", "map": _map_value(world), "cell": _cell_value(world), + "ok": world.map_id() == Vector2i(0, GEN1_HALL_OF_FAME) and world.state.hall_of_fame(), + "hall_of_fame_flag": world.state.hall_of_fame(), "party": _party_species(save), + "badges": world.state.badge_count(), + }) + if not bool(path[-1]["ok"]): + return {"ok": false, "path": path, "reason": "hall_of_fame: the walk ended on %s with the flag %s" % [ + _map_value(world), world.state.hall_of_fame()]} + return {"ok": true} + + +## The gym's pads to Sabrina, and the MARSHBADGE. +func _gen1_saffron_gym_leg( + world: Gen2WorldAPI, save: Gen2SaveData, random: RandomNumberGenerator, data: GameData, + path: Array +) -> Dictionary: + var legs: Array = [[GEN1_SAFFRON_GYM, "saffron_gym_entry", GEN1_SAFFRON_GYM_DOOR]] + for pad: int in GEN1_SAFFRON_GYM_PADS.size(): + legs.append([GEN1_SAFFRON_GYM, "saffron_gym_pad_%d" % (pad + 1), GEN1_SAFFRON_GYM_PADS[pad]]) + var stepped: Dictionary = _gen1_warp_legs(path, world, save, random, data, legs) + if not bool(stepped["ok"]): + return stepped + var sabrina: Dictionary = _gen1_talk_to(world, GEN1_SABRINA, save, random, data) + stepped = _gen1_step(path, "saffron_gym_sabrina", world, sabrina, { + "party": _party_species(save), "items": _named_items(data, world.state.items()), + "badges": world.state.badge_count(), + }) + if not bool(stepped["ok"]): + return stepped + for flag: Array in [ + [GEN1_EVENT_BEAT_SABRINA, "EVENT_BEAT_SABRINA"], [GEN1_EVENT_GOT_TM46, "EVENT_GOT_TM46"], + ]: + stepped = _gen1_flag_leg(path, "saffron_gym_sabrina", world, int(flag[0]), String(flag[1])) + if not bool(stepped["ok"]): + return stepped + if not world.state.is_engine_flag_active(Gen2WorldState.gen1_badge_flag(GEN1_BIT_MARSHBADGE)): + return {"ok": false, "path": path, "reason": "saffron_gym_sabrina: MARSHBADGE is clear"} + return {"ok": true} diff --git a/tools/validate.gd b/tools/validate.gd index dfedfc55..c59b59b4 100644 --- a/tools/validate.gd +++ b/tools/validate.gd @@ -52,15 +52,25 @@ const GROUPS: Dictionary = { } +var _names: PackedStringArray = [] + + func _initialize() -> void: var topics: PackedStringArray = _available() - var names: PackedStringArray = _requested(topics) - if names.is_empty(): + _names = _requested(topics) + if _names.is_empty(): print("Topics: %s" % ", ".join(topics)) print("Groups: %s, all" % ", ".join(PackedStringArray(GROUPS.keys()))) quit(2) - return + +## On the first frame rather than in `_initialize`: the root is not inside the +## tree until the loop starts, so a screen a topic adds there is never ready. +func _process(_delta: float) -> bool: + if _names.is_empty(): + return true + var names: PackedStringArray = _names + _names = [] var failed: PackedStringArray = [] for name: String in names: var run := CheckRun.new() @@ -87,6 +97,7 @@ func _initialize() -> void: if not failed.is_empty(): printerr("FAILED: %s" % ", ".join(failed)) quit(1 if not failed.is_empty() else 0) + return true ## Every topic on disk, so a new check file needs no registration.