diff --git a/assets/logo/gen1recomp_cover.png b/assets/logo/gen1recomp_cover.png index daf0543c..4e077216 100644 Binary files a/assets/logo/gen1recomp_cover.png and b/assets/logo/gen1recomp_cover.png differ diff --git a/data/scripts/flavor/pewter_city.lua b/data/scripts/flavor/pewter_city.lua index e01f0e87..b5047092 100644 --- a/data/scripts/flavor/pewter_city.lua +++ b/data/scripts/flavor/pewter_city.lua @@ -1,15 +1,11 @@ -- Pewter City flavor dialogue (pokered/scripts/PewterCity.asm). -- PewterCity_TextPointers text_asm bodies for the SUPER_NERD1 museum --- guide, SUPER_NERD2 garden nerd, and the leaving-east YOUNGSTER. +-- guide and SUPER_NERD2 garden nerd. -- --- The escort choreography (SUPER_NERD1 walking the player to the --- museum, YOUNGSTER walking the player to the gym) is scripted NPC --- movement + a wPewterCityCurScript state machine that steers the --- player off-map; the YOUNGSTER's escort to the gym is handled on this --- map by story5.lua's onStep gate at the east exit (before --- EVENT_BEAT_BROCK), while the SUPER_NERD1 museum escort is not ported. --- Here we only port the real YES/NO-branched flavor text these NPCs --- speak when talked to. +-- The YOUNGSTER's gym escort (talk + east-exit onStep) lives in +-- story5.lua so the lockstep RLE walk is not overwritten by this +-- flavor merge. SUPER_NERD1's museum escort is not ported; only the +-- YES/NO-branched flavor text is here. local M = {} @@ -61,15 +57,6 @@ M.PEWTER_CITY = { end) end, - -- PewterCityYoungsterText (scripts/PewterCity.asm): the "follow - -- me" line the youngster says when the player is stopped from - -- leaving Pewter east before beating Brock; the actual gate/step - -- block is handled by story5.lua's onStep for this map. - TEXT_PEWTERCITY_YOUNGSTER = function(game, ow, npc, done) - local t = text(game) - push(game, t._PewterCityYoungsterYoureATrainerFollowMeText - or "You're a trainer\nright? BROCK's\nlooking for new\nchallengers!\nFollow me!", done) - end, }, } diff --git a/data/scripts/story5.lua b/data/scripts/story5.lua index bd2c55f3..e460a953 100644 --- a/data/scripts/story5.lua +++ b/data/scripts/story5.lua @@ -262,38 +262,180 @@ M.CINNABAR_ISLAND = { -- Pewter's youngster stops you leaving east before Brock is beaten and -- escorts you to the gym (scripts/PewterCity.asm -- PewterCityCheckPlayerLeavingEastScript / --- PewterCityYoungsterShowsPlayerGymScript, engine/events/pewter_guys.asm --- PewterGymGuyCoords): the "follow me" lines, then the player is walked --- west along the road to the front of the PEWTER_GYM door at (16,17). +-- PewterCityYoungsterShowsPlayerGymScript, engine/overworld/auto_movement.asm +-- PewterGymGuyMovementScriptPointerTable, engine/events/pewter_guys.asm +-- PewterGymGuyCoords). Same lockstep style as Oak's lab escort: the +-- youngster walks RLEList_PewterGymGuy while the player plays the +-- reverse of RLEList_PewterGymPlayer with a PewterGuys positioning +-- preamble. Ends at (11,18) / (12,18) by the gym, not phasing through +-- the building. +local pewterEscort = {} + +-- RLEList_PewterGymGuy (NPC directions play forward) +pewterEscort.guySteps = { + "down", "down", + "left", "left", "left", "left", "left", "left", "left", "left", + "left", "left", "left", "left", "left", "left", "left", + "up", "up", "up", "up", "up", + "left", "left", "left", "left", "left", "left", "left", "left", + "left", "left", "left", + "down", "down", "down", "down", "down", + "right", "right", "right", +} + +-- Walk home: reverse of guySteps with opposite facings (gym → spawn). +do + local opp = { up = "down", down = "up", left = "right", right = "left" } + local ret = {} + for i = #pewterEscort.guySteps, 1, -1 do + ret[#ret + 1] = opp[pewterEscort.guySteps[i]] + end + pewterEscort.guyReturnSteps = ret +end + +-- RLEList_PewterGymPlayer before reverse / PewterGuys (NO_INPUT, RIGHT×2, +-- DOWN×5, LEFT×11, UP×5, LEFT×15) +pewterEscort.playerRle = { + "NO", + "right", "right", + "down", "down", "down", "down", "down", + "left", "left", "left", "left", "left", "left", "left", "left", + "left", "left", "left", + "up", "up", "up", "up", "up", + "left", "left", "left", "left", "left", "left", "left", "left", + "left", "left", "left", "left", "left", "left", "left", +} + +-- PewterGymGuyCoords: (x, y) -> positioning moves written after the RLE +-- (played in reverse; $00 pauses are one overworld frame ≈ 1/8 tile for +-- the NPC, so eight of them ≈ one guy head-start step) +pewterEscort.preambles = { + ["34,16"] = { "left", "down", "down", "right" }, + ["35,17"] = { "left", "down", "right", "left" }, + ["37,18"] = { "left", "left", "left", + "NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO" }, + ["37,19"] = { "left", "left", "up", "left" }, + ["36,17"] = { "left", "down", "left", + "NO", "NO", "NO", "NO", "NO", "NO", "NO", "NO" }, +} + +-- Realized player path for a trigger tile: PewterGuys overwrites the +-- last RLE byte and appends the preamble, then simulated joypad plays +-- high→low (reverse). Leading NO×8 collapses to guyHeadStart=1; the +-- trailing end-of-list NO is dropped (one-frame pause). +function pewterEscort.playerPlan(x, y) + local pre = pewterEscort.preambles[x .. "," .. y] + if not pre then return nil end + local buf = {} + for i, d in ipairs(pewterEscort.playerRle) do buf[i] = d end + buf[#buf] = pre[1] + for i = 2, #pre do buf[#buf + 1] = pre[i] end + local path = {} + for i = #buf, 1, -1 do path[#path + 1] = buf[i] end + local head = 0 + while path[head + 1] == "NO" do head = head + 1 end + local tail = #path + while tail > head and path[tail] == "NO" do tail = tail - 1 end + local steps = {} + for i = head + 1, tail do steps[#steps + 1] = path[i] end + return { steps = steps, guyHeadStart = math.floor(head / 8) } +end + +local function pewterGymEscort(game, ow) + if ow.runner:isRunning() or #ow.scriptMoves > 0 then return end + local x, y = ow.player.cellX, ow.player.cellY + local plan = pewterEscort.playerPlan(x, y) + local Music = require("src.core.Music") + local t = text(game) + local follow = t._PewterCityYoungsterYoureATrainerFollowMeText + or "You're a trainer\nright? BROCK's\nlooking for new\nchallengers!\nFollow me!" + -- PewterGuys only has entries for five tiles; an unmatched talk tile + -- (e.g. (36,16) east of him) just gets the follow-me line, same as a + -- failed coords lookup would refuse to arm the walk. + if not plan then + push(game, follow) + return + end + local guy = ow:npcByIndex(5) -- PEWTERCITY_YOUNGSTER + local guySteps = pewterEscort.guySteps + local head = plan.guyHeadStart + + -- After the walk: face the player, restore map music, "Go take on + -- BROCK", then retrace RLEList_PewterGymGuy back to his spawn (35,16). + -- (pokered teleports him via MovementData_PewterGymGuyExit; we walk + -- the same route home instead. Brock victory still HideObject's him.) + local function walkHome() + if not guy then return end + local ret = pewterEscort.guyReturnSteps + local i = 0 + local function tick() + i = i + 1 + if not ret[i] then + guy.facing = "down" + return + end + ow:scriptMove(guy, ret[i], 1, tick) + end + tick() + end + + local function afterWalk() + if guy then guy.facing = "left" end + Music.playMap(game.data, "PEWTER_CITY") + push(game, t._PewterCityYoungsterGoTakeOnBrockText + or "Go take on BROCK\nat the GYM first!", walkHome) + end + + local function lockstep() + local i = 0 + local function tick() + i = i + 1 + local ps = plan.steps[i] + if not ps then + afterWalk() + return + end + local gs = guySteps[head + i] + if guy and gs then ow:scriptMove(guy, gs, 1) end + ow:scriptMove(ow.player, ps, 1, tick) + end + tick() + end + + local function beginWalk() + Music.play(game.data, "Music_MuseumGuy") + if guy and head > 0 then + local h = 0 + local function headTick() + h = h + 1 + if h > head then lockstep(); return end + ow:scriptMove(guy, guySteps[h], 1, headTick) + end + headTick() + else + lockstep() + end + end + + push(game, follow, beginWalk) +end + M.PEWTER_CITY = { + escort = pewterEscort, + -- PewterCityYoungsterText: talking also arms the gym escort script + talk = { + TEXT_PEWTERCITY_YOUNGSTER = function(game, ow, npc, done) + pewterGymEscort(game, ow) + if done then done() end + end, + }, onStep = function(game, ow, x, y) if game.save.flags.EVENT_BEAT_BROCK then return false end if ow.runner:isRunning() or #ow.scriptMoves > 0 then return false end if not inCoords({ { 35, 17 }, { 36, 17 }, { 37, 18 }, { 37, 19 } }, x, y) then return false end - local t = text(game) - -- walk up onto the road row (y=17), west to one tile east of the gym - -- door, then drop below the door and turn to face it - local steps = {} - for _ = 1, y - 17 do steps[#steps + 1] = "up" end - for _ = 1, x - 17 do steps[#steps + 1] = "left" end - steps[#steps + 1] = "down" - steps[#steps + 1] = "left" - local function walk(i) - if not steps[i] then - ow.player.facing = "up" - return - end - ow:scriptMove(ow.player, steps[i], 1, function() walk(i + 1) end) - end - push(game, t._PewterCityYoungsterYoureATrainerFollowMeText - or "Hey! You're a\ntrainer, right?", function() - push(game, t._PewterCityYoungsterGoTakeOnBrockText - or "Go take on BROCK\nat the GYM first!", function() - walk(1) - end) - end) + pewterGymEscort(game, ow) return true end, } diff --git a/data/scripts/victories.lua b/data/scripts/victories.lua index 2741ffea..45c620c5 100644 --- a/data/scripts/victories.lua +++ b/data/scripts/victories.lua @@ -22,9 +22,16 @@ local function range(prefix, first, last) end return { + -- PewterGym.asm .gymVictory also HideObject TOGGLE_GYM_GUY + -- (PEWTERCITY_YOUNGSTER) and TOGGLE_ROUTE_22_RIVAL_1 so the east-exit + -- escort NPC and the first Route 22 rival stay gone after the badge. ["OPP_BROCK#1"] = { badge = "BOULDERBADGE", flag = "EVENT_BEAT_BROCK", item = "TM_BIDE", - deactivate = { "EVENT_BEAT_PEWTER_GYM_TRAINER_0" } }, + deactivate = { "EVENT_BEAT_PEWTER_GYM_TRAINER_0" }, + hide = { + { "PEWTER_CITY", "PEWTERCITY_YOUNGSTER" }, + { "ROUTE_22", "ROUTE22_RIVAL1" }, + } }, ["OPP_MISTY#1"] = { badge = "CASCADEBADGE", flag = "EVENT_BEAT_MISTY", item = "TM_BUBBLEBEAM", deactivate = range("EVENT_BEAT_CERULEAN_GYM_TRAINER_", 0, 1) }, diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index 8196ffaf..82c888f1 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -2351,6 +2351,8 @@ end -- Badges/items awarded after specific battles (data/scripts/victories.lua). -- `deactivate` retires unfought gym/dojo trainers the way the originals' -- SetEvent / SetEventRange do after the leader victory. +-- `hide` is { { mapId, objName }, ... } — HideObject on those toggles +-- (e.g. Brock victory clears PEWTERCITY_YOUNGSTER / ROUTE22_RIVAL1). function OverworldState:checkVictoryRewards(trainerClass, partyIndex) local victories = require("data.scripts.victories") local reward = victories[trainerClass .. "#" .. tostring(partyIndex or 1)] @@ -2364,6 +2366,13 @@ function OverworldState:checkVictoryRewards(trainerClass, partyIndex) Game.save.flags[flag] = true end end + if reward.hide then + local Commands = require("src.script.Commands") + local ctx = { game = Game, save = Game.save, overworld = self } + for _, entry in ipairs(reward.hide) do + Commands.hide_object(ctx, entry[1], entry[2]) + end + end local lines = {} if reward.badge then Game.save.inventory[reward.badge] = 1 diff --git a/tests/parity_A.lua b/tests/parity_A.lua index e082401f..09c6786d 100644 --- a/tests/parity_A.lua +++ b/tests/parity_A.lua @@ -338,6 +338,15 @@ do end check(pewterNpc and ow:trainerDefeated(pewterNpc), "unfought Pewter gym trainer is defeated after badge") + -- PewterGym.asm .gymVictory HideObject TOGGLE_GYM_GUY / + -- TOGGLE_ROUTE_22_RIVAL_1 (persists via objectToggles) + check(Game.save.objectToggles + and Game.save.objectToggles.PEWTER_CITY + and Game.save.objectToggles.PEWTER_CITY.PEWTERCITY_YOUNGSTER == false, + "Brock victory hides PEWTERCITY_YOUNGSTER") + check(Game.save.objectToggles.ROUTE_22 + and Game.save.objectToggles.ROUTE_22.ROUTE22_RIVAL1 == false, + "Brock victory hides ROUTE22_RIVAL1") while Game.stack:top() do Game.stack:pop() end Game.save = SaveData.newGame() diff --git a/tests/parity_pewter_escort.lua b/tests/parity_pewter_escort.lua new file mode 100644 index 00000000..67344631 --- /dev/null +++ b/tests/parity_pewter_escort.lua @@ -0,0 +1,117 @@ +-- Parity test: Pewter City youngster gym escort paths. +-- Self-contained: run via `luajit tests/parity_pewter_escort.lua`; also +-- dofile'd by tests/run_tests.lua's aggregator. +-- +-- Sources: scripts/PewterCity.asm, engine/overworld/auto_movement.asm +-- (RLEList_PewterGymPlayer / RLEList_PewterGymGuy), +-- engine/events/pewter_guys.asm (PewterGymGuyCoords). +package.path = "./?.lua;./?/init.lua;" .. package.path +if not _G.love then _G.love = require("tests.love_stub") end +local Data = require("src.core.Data") +if not (Data.maps and Data.maps.PEWTER_CITY) then Data:load() end +local S = require("tests.harness").suite("parity pewter escort") +local check, eq = S.check, S.eq + +local mapScripts = require("data.scripts.init") +local pewter = mapScripts.get("PEWTER_CITY") +check(pewter and pewter.escort, "PEWTER_CITY exposes the escort tables") +local escort = pewter.escort + +local function joined(t) return table.concat(t, ",") end + +local D = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } } + +eq(#escort.guySteps, 41, "youngster takes 41 steps (RLEList_PewterGymGuy)") +eq(joined({ escort.guySteps[1], escort.guySteps[2], escort.guySteps[#escort.guySteps] }), + "down,down,right", "guy path starts with DOWN×2 and ends RIGHT") +eq(#escort.guyReturnSteps, 41, "return path mirrors the escort") +do + local opp = { up = "down", down = "up", left = "right", right = "left" } + eq(escort.guyReturnSteps[1], opp[escort.guySteps[#escort.guySteps]], + "return starts with opposite of escort's last step") + local gx, gy = 12, 18 + for _, d in ipairs(escort.guyReturnSteps) do + gx, gy = gx + D[d][1], gy + D[d][2] + end + check(gx == 35 and gy == 16, + "return path from the gym lands on his spawn (35,16)") +end + +-- every east-exit / talk tile lands both sprites by the gym road +local triggers = { { 35, 17 }, { 36, 17 }, { 37, 18 }, { 37, 19 }, { 34, 16 } } +for _, pos in ipairs(triggers) do + local px, py = pos[1], pos[2] + local plan = escort.playerPlan(px, py) + check(plan, ("playerPlan for (%d,%d)"):format(px, py)) + local gx, gy = 35, 16 + local gi = 0 + for _ = 1, plan.guyHeadStart do + gi = gi + 1 + local d = escort.guySteps[gi] + gx, gy = gx + D[d][1], gy + D[d][2] + end + for i, ps in ipairs(plan.steps) do + px, py = px + D[ps][1], py + D[ps][2] + local gs = escort.guySteps[plan.guyHeadStart + i] + if gs then + gx, gy = gx + D[gs][1], gy + D[gs][2] + end + end + eq(px, 11, ("player from (%d,%d) ends at x=11"):format(pos[1], pos[2])) + eq(py, 18, ("player from (%d,%d) ends at y=18"):format(pos[1], pos[2])) + eq(gx, 12, ("guy from (%d,%d) ends at x=12"):format(pos[1], pos[2])) + eq(gy, 18, ("guy from (%d,%d) ends at y=18"):format(pos[1], pos[2])) +end + +-- the short east-road trigger uses no head-start; the (36,17)/(37,18) +-- tiles pause for one guy step (eight NO_INPUT frames) +eq(escort.playerPlan(35, 17).guyHeadStart, 0, "(35,17) no head-start") +eq(escort.playerPlan(36, 17).guyHeadStart, 1, "(36,17) one-step head-start") +eq(escort.playerPlan(37, 18).guyHeadStart, 1, "(37,18) one-step head-start") + +-- path stays off the PEWTER_GYM door warp and the blocked gym building +-- cells: sample the (35,17) walk and confirm no step lands on (16,17) +do + local plan = escort.playerPlan(35, 17) + local px, py = 35, 17 + local gx, gy = 35, 16 + for i, ps in ipairs(plan.steps) do + px, py = px + D[ps][1], py + D[ps][2] + local gs = escort.guySteps[i] + if gs then gx, gy = gx + D[gs][1], gy + D[gs][2] end + check(not (px == 16 and py == 17), + ("player beat %d does not phase onto gym door"):format(i)) + check(not (gx == 16 and gy == 17), + ("guy beat %d does not phase onto gym door"):format(i)) + end +end + +-- gym door warp is still where the player is meant to walk afterward +local MapLoader = require("src.world.MapLoader") +local city = MapLoader.load(Data, "PEWTER_CITY") +local w = city:warpAtCell(16, 17) +check(w and w.def.destMap == "PEWTER_GYM", "(16,17) is the Pewter Gym door") + +-- object spawn matches pokered object_event 35, 16 +local young +for _, o in ipairs(Data.maps.PEWTER_CITY.objects) do + if o.name == "PEWTERCITY_YOUNGSTER" then young = o; break end +end +check(young and young.x == 35 and young.y == 16, + "PEWTERCITY_YOUNGSTER spawns at (35,16)") + +-- Brock victory permanently HideObject's him (PewterGym.asm .gymVictory) +do + local victories = require("data.scripts.victories") + local hide = victories["OPP_BROCK#1"] and victories["OPP_BROCK#1"].hide + check(hide, "OPP_BROCK#1 lists objects to hide") + local found + for _, entry in ipairs(hide or {}) do + if entry[1] == "PEWTER_CITY" and entry[2] == "PEWTERCITY_YOUNGSTER" then + found = true + end + end + check(found, "Brock victory hides PEWTERCITY_YOUNGSTER") +end + +S.finish()