Compare commits

...

15 Commits

Author SHA1 Message Date
bryanthaboi fea81f03fc Merge branch 'dev' 2026-08-01 21:10:08 -04:00
bryanthaboi b549e319c2 Update README.md 2026-08-01 20:54:08 -04:00
bryanthaboi 378246728b Merge pull request #628 from spiritsnails/fix/oaks-lab-rival-music 2026-08-01 19:57:10 -04:00
bryanthaboi f9adeca5ca Merge pull request #618 from castdrian/metal 2026-08-01 19:56:29 -04:00
bryanthaboi 838f87c224 Merge pull request #619 from ShaneMcGovernIE/fix/viridian-old-man-yellow-617 2026-08-01 19:55:49 -04:00
spiritsnails 5b519165fc Fix #596: missing rival battle music in Oak's Lab
The Yellow port (oaks_lab_yellow.lua) already swaps in Music_MeetRival
for the "Hey wait! Let's check out our Pokemon!" challenge and restores
Music_OaksLab once the rival walks out, but the Red/Blue script never
did, leaving the lab battle silent before and after the fight.
2026-08-01 17:41:35 -06:00
Adrian Castro 1a2b23a066 ci: configure dependabot for actions 2026-08-02 00:26:48 +02:00
Adrian Castro fe16b3259c ci(ios): comment artifacts after pull request builds 2026-08-02 00:01:53 +02:00
Adrian Castro 2e46954fe0 ci(ios): use release signing in verification 2026-08-01 23:56:04 +02:00
Adrian Castro 00d13d3c35 ci(ios): comment release artifacts on pull requests 2026-08-01 23:43:12 +02:00
Adrian Castro 1b8b3ad538 ci(ios): build device release on canonical repo 2026-08-01 23:43:11 +02:00
Adrian Castro 5f75cfd691 ci: verify release builds on fork runners 2026-08-01 23:43:11 +02:00
Adrian Castro 5f89def2ce ci: select hosted runners for forks 2026-08-01 23:43:08 +02:00
Adrian Castro 7c26eb9a24 ci(ios): gate hosted build on iOS changes 2026-08-01 23:43:07 +02:00
Shane McGovern d951fe8fc5 Fix Viridian old man tutorial dialog in Yellow (#617) 2026-08-01 22:38:09 +01:00
12 changed files with 614 additions and 12 deletions
+6
View File
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
+79 -2
View File
@@ -10,8 +10,6 @@ name: ci
# The T3 content tier asserts Pokemon Red facts; scripts/test.sh detects
# data/generated/ is absent and skips it rather than failing.
#
# Runs alongside release.yml, which is untouched by this file.
on:
push:
# Integration branch + release branch. PRs already run via pull_request
@@ -24,7 +22,86 @@ concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
ios-changes:
name: detect iOS changes
runs-on: ubuntu-latest
outputs:
changed: ${{ steps.paths.outputs.changed }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: paths
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.sha }}
run: |
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
echo "changed=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(mobile/ios/|scripts/build_ios\.sh$|\.github/workflows/(ci|release)\.yml$)'; then
echo "changed=true" >> "$GITHUB_OUTPUT"
else
echo "changed=false" >> "$GITHUB_OUTPUT"
fi
ios-build:
name: iOS build
needs: ios-changes
if: needs.ios-changes.outputs.changed == 'true'
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
outputs:
ipa_url: ${{ steps.upload-ipa.outputs.artifact-url }}
steps:
- uses: actions/checkout@v4
- name: import signing certificate
if: github.repository == 'bryanthaboi/gen1recomp'
run: |
keychain_path="$RUNNER_TEMP/gen1recomp-ci-signing.keychain-db"
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
p12="$ci_dir/signing.p12"
passfile="$ci_dir/signing.pass"
[ -f "$p12" ] && [ -f "$passfile" ] || exit 1
p12pw="$(cat "$passfile")"
kcpw="$(openssl rand -base64 24)"
echo "::add-mask::$kcpw"
security delete-keychain "$keychain_path" 2>/dev/null || true
security create-keychain -p "$kcpw" "$keychain_path"
security set-keychain-settings "$keychain_path"
security unlock-keychain -p "$kcpw" "$keychain_path"
security import "$p12" -P "$p12pw" -k "$keychain_path" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$kcpw" "$keychain_path" >/dev/null
existing="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g')"
security list-keychains -d user -s "$keychain_path" $existing
- name: install xcbeautify
run: brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
- name: build iOS release
env:
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
run: |
if [ "$CANONICAL_REPOSITORY" = true ]; then
scripts/build_ios.sh --fetch --device --release
else
scripts/build_ios.sh --fetch --release
fi
- name: upload iOS release artifact
id: upload-ipa
if: github.repository == 'bryanthaboi/gen1recomp'
uses: actions/upload-artifact@v4
with:
name: gen1recomp-ios-ipa
path: dist/ios/gen1recomp.ipa
if-no-files-found: error
retention-days: 7
- name: clean up signing keychain
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
run: security delete-keychain "$RUNNER_TEMP/gen1recomp-ci-signing.keychain-db" 2>/dev/null || true
headless:
name: headless suites (no ROM)
runs-on: ubuntu-latest
@@ -0,0 +1,43 @@
name: iOS artifact comment
on:
workflow_run:
workflows: [ci]
types: [completed]
permissions:
contents: read
pull-requests: write
jobs:
comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- id: artifact
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
run: |
artifact_id="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[] | select(.name == "gen1recomp-ios-ipa") | .id')"
[ -n "$artifact_id" ] || exit 0
head_owner="${HEAD_REPOSITORY%%/*}"
pr_number="$(gh api "repos/$GITHUB_REPOSITORY/pulls?state=open&head=$head_owner:$HEAD_BRANCH" --jq '.[0].number // empty')"
[ -n "$pr_number" ] || exit 0
echo "artifact_url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$artifact_id" >> "$GITHUB_OUTPUT"
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
- name: comment iOS artifact
if: steps.artifact.outputs.pr_number != ''
uses: thollander/actions-comment-pull-request@v3
with:
message: |
#### iOS Release IPA
- [Download gen1recomp.ipa](${{ steps.artifact.outputs.artifact_url }})
<sub>Automatically generated. [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }})</sub>
pr-number: ${{ steps.artifact.outputs.pr_number }}
comment-tag: ios-build-result
github-token: ${{ github.token }}
+15 -7
View File
@@ -42,7 +42,7 @@ concurrency:
jobs:
release:
runs-on: [self-hosted, macOS]
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
steps:
# The self-hosted runner lives under the machine owner's home
@@ -127,6 +127,7 @@ jobs:
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Import signing certificate into a temporary keychain
if: github.repository == 'bryanthaboi/gen1recomp'
run: |
set -euo pipefail
KEYCHAIN_PATH="$RUNNER_TEMP/pokemon-signing.keychain-db"
@@ -182,13 +183,17 @@ jobs:
brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
- name: Build iOS
env:
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
run: |
set -euo pipefail
# Device Release IPA; signs with the Apple Development identity on
# the runner (auto team detection). Users on other Apple IDs still
# re-sign or build via docs/ios-install.md.
scripts/build_ios.sh --fetch --device --release \
--version "${{ steps.ver.outputs.version }}"
if [ "$CANONICAL_REPOSITORY" = true ]; then
scripts/build_ios.sh --fetch --device --release \
--version "${{ steps.ver.outputs.version }}"
else
scripts/build_ios.sh --fetch --release \
--version "${{ steps.ver.outputs.version }}"
fi
- name: Build Anbernic RG34XXSP port
run: |
@@ -198,6 +203,7 @@ jobs:
./build-rg34xxsp.sh --version "${{ steps.ver.outputs.version }}"
- name: Notarize & staple macOS app
if: github.repository == 'bryanthaboi/gen1recomp'
run: |
set -euo pipefail
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
@@ -232,6 +238,7 @@ jobs:
echo "Notarized + stapled ✓"
- name: Stage release assets
if: github.repository == 'bryanthaboi/gen1recomp'
id: assets
run: |
set -euo pipefail
@@ -271,6 +278,7 @@ jobs:
cat "$outdir/sha256sums.txt"
- name: Publish GitHub Release
if: github.repository == 'bryanthaboi/gen1recomp'
env:
GH_TOKEN: ${{ github.token }}
run: |
@@ -376,6 +384,6 @@ jobs:
echo "Published release $tag"
- name: Clean up signing keychain
if: always()
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
run: |
security delete-keychain "$RUNNER_TEMP/pokemon-signing.keychain-db" 2>/dev/null || true
+16
View File
@@ -4,6 +4,9 @@ A native LÖVE2D recreation of Poke Red, Blue and Yellow. The engine and map
behavior are hand-written Lua; game data and graphics are decoded from a ROM
supplied by the player.
> [!WARNING]
> **We are NOT affiliated with the website `gen1recomp[.]com`** That website is not run by this project, was not authorized by us, and we have no idea who operates it. It is impersonating this project; do not download anything from it, and treat anything it hosts or claims as untrustworthy. Even if the site currently links back to this repository, the people behind it can change its content at any time, so nothing on it should ever be trusted. This GitHub repository and the Discord linked below are the only official sources for this project.
<p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p>
**SUPPORT / ANNOUNCEMENTS / MODS:** [Discord](https://bois.icu)
@@ -75,6 +78,19 @@ The packaged app contains neither a ROM nor pre-extracted game data. Music,
sound effects, and cries are synthesized while the game runs from compact
audio channel programs copied out of the verified ROM.
### A note on Windows Defender warnings
Windows Defender sometimes flags the Windows build with a generic
machine-learning detection such as `Trojan:Win32/Wacatac!ml` (#621). This is
a known false positive: the exe is the official LÖVE runtime with the game
archive appended (the standard way LÖVE games ship), and Defender's
heuristics distrust unsigned executables with appended data. Every release
publishes SHA-256 checksums (`sha256sums.txt`) so you can verify your
download, and you can confirm a flagged file yourself on
[VirusTotal](https://www.virustotal.com), where these builds come back clean
on every engine except Defender's heuristic. False positives are reported to
Microsoft as they come up.
## Controls
+2 -1
View File
@@ -48,7 +48,8 @@ end
if GameVersion.isYellow() then
for _, file in ipairs({ "data.scripts.yellow_gifts",
"data.scripts.yellow_jessie_james",
"data.scripts.yellow_beach_house" }) do
"data.scripts.yellow_beach_house",
"data.scripts.yellow_viridian_old_man" }) do
for mapId, mod in pairs(require(file)) do
MapScripts.attachBase(mapId, mod)
end
+7
View File
@@ -254,7 +254,12 @@ return {
and y >= 6 then
local rival = ow:npcByIndex(1)
if not rival then return false end
-- OaksLabRivalChallengesPlayerScript swaps in the rival encounter
-- fanfare for the taunt/challenge exchange, same as the Yellow port
-- (oaks_lab_yellow.lua); it was silently dropped here (#596).
local rows = {
{ "stop_music" },
{ "play_music", "Music_MeetRival" },
{ "show_text", "_OaksLabRivalIllTakeYouOnText" }, -- 1
}
-- the rival routes to a free cell beside the player
@@ -294,6 +299,8 @@ return {
table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" })
table.insert(rows, { "move_npc_to", 1, 4, 11 })
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
-- restore the lab theme once he's walked out, same as the Yellow port
table.insert(rows, { "play_music", "Music_OaksLab" })
ow.runner:run(rows, { npc = rival })
return true
end
+5 -1
View File
@@ -71,8 +71,12 @@ return {
{ "show_text", "_OaksLabRivalLeaveItAllToMeText" },
{ "set_flag", "EVENT_GOT_POKEDEX" },
{ "set_flag", "EVENT_OAK_GOT_PARCEL" },
-- OaksLabOakGivesPokedexScript: HideObject TOGGLE_LYING_OLD_MAN /
-- ShowObject TOGGLE_OLD_MAN_2 -- Yellow's tutorial old man stands
-- on the sleeper's cell (18,9); the Red/Blue walker OLD_MAN at
-- (17,5) never appears in Yellow (#617)
{ "hide_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN_SLEEPY" },
{ "show_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN" },
{ "show_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN2" },
{ "stop_music" },
{ "play_music", "Music_MeetRival" },
{ "move_npc_to", RIVAL, 4, 7 },
+145
View File
@@ -0,0 +1,145 @@
-- The Viridian City old-man catch tutorial, Yellow's way
-- (pokeyellow scripts/ViridianCity.asm, scripts/ViridianCity_2.asm,
-- scripts/OaksLab.asm). Registered on top of the shared tables by
-- data/scripts/init.lua on a Yellow boot.
--
-- Yellow has TWO gambler objects where Red/Blue have one:
-- * VIRIDIANCITY_OLD_MAN at (17,5) -- a Red/Blue leftover; its
-- toggle stays OFF forever in Yellow, no script ever shows it.
-- * VIRIDIANCITY_OLD_MAN2 at (18,9) -- replaces the sleeper the
-- moment the Pokédex is given (OaksLabOakGivesPokedexScript:
-- HideObject TOGGLE_LYING_OLD_MAN / ShowObject TOGGLE_OLD_MAN_2).
--
-- This is the tutorial old man. The Red/Blue "Are you in a hurry?"
-- yes/no script must NOT run against him: Yellow's
-- _ViridianCityOldManHadMyCoffeeNowText is the apology speech ("I've had
-- my coffee now ... I'll show you how to catch POKéMON as my apology"),
-- and the shared story.lua TEXT_VIRIDIANCITY_OLD_MAN rows hang an
-- invented yes/no over it -- YES printed the TimeIsMoney alias
-- (_ViridianCityOldManLosingMyTouchText) and NO ran the demo, every
-- talk, forever (#617).
--
-- The real flow (ViridianCityCheckWaitingOldMan + ViridianCityOldMan2Text
-- + ViridianCityOldManInitialCatchTrainingScript + ...EndInitial... +
-- ViridianCityPostInitialCatchTraining): stepping into (19,9) -- the gap
-- east of the sleeper's cell -- faces the old man right and the player
-- left, prints the apology, and without any choice runs the demo battle
-- (BATTLE_TYPE_OLD_MAN, RATTATA lvl 5). After it, the same text pointer
-- now prints _ViridianCityOldManLosingMyTouchText ("That didn't work!
-- I must be losing my touch."), the old man walks off (down 6 with the
-- player on (19,9), right 1 otherwise, Pikachu nudged out of the way
-- first) and TOGGLE_OLD_MAN_2 hides. A direct talk does the same.
local M = {}
local OLD_MAN2 = "VIRIDIANCITY_OLD_MAN2"
-- Capture the FUNCTION, not the table: attachBase stores the module
-- table itself, so once this file's onStep is attached the table's slot
-- points back here -- delegating through the table would self-recurse.
-- story5's VIRIDIAN_CITY.onStep chains story.lua's sleeping-old-man
-- gate and its own gym-lock step (same pattern as yellow_jessie_james).
local baseViridianStep = require("data.scripts.story5").VIRIDIAN_CITY.onStep
-- pokeyellow text/ViridianCity.asm, _ViridianCityOldManHadMyCoffeeNowText
-- and _ViridianCityOldManLosingMyTouchText, spelled with the extractor's
-- markers (line -> \n, cont -> \v, para -> \f)
local function text(game)
return {
apology = game.data.text._ViridianCityOldManHadMyCoffeeNowText
or "Ahh, I've had my\ncoffee now and I\vfeel great!\fSure, you can go\n"
.. "through!\fI'm sorry I was\nso rude to you!\fI see you're using\n"
.. "a POKéDEX.\fI'll show you how\nto catch POKéMON\vas my apology.",
losingMyTouch = game.data.text._ViridianCityOldManLosingMyTouchText
or "That didn't work!\nI must be losing\vmy touch.\fI've run out of\n"
.. "POKé BALLs too.\fI have to get some\nat POKéMON MART.",
}
end
-- The row list for the initial-tutorial branch, keyed by where the
-- player stands when the battle ends (ViridianCityPostInitialCatchTraining
-- reads wXCoord: (19,9) walks the old man down the corridor, anywhere
-- else walks him right 1 after moving the follower Pikachu aside).
local function oldMan2Rows(game, ow, npc)
local rows = {
{ "show_text", "_ViridianCityOldManHadMyCoffeeNowText" },
{ "old_man_demo" },
{ "set_flag", "EVENT_COMPLETED_CATCH_TRAINING" },
{ "show_text", "_ViridianCityOldManLosingMyTouchText" },
}
if ow.player and ow.player.cellX == 19 then
rows[#rows + 1] =
{ "walk_npc", npc.def.index,
{ "down", "down", "down", "down", "down", "down" } }
else
-- ViridianCityMovePikachu (scripts/ViridianCity_2.asm): Pikachu
-- steps out of the old man's way before he turns right
local PikachuFollower = require("src.world.PikachuFollower")
local pika = ow and PikachuFollower.current(ow)
if pika then
rows[#rows + 1] = { "walk_npc", pika.def.index, { "right" } }
end
rows[#rows + 1] = { "walk_npc", npc.def.index, { "right" } }
end
rows[#rows + 1] =
{ "hide_object", "VIRIDIAN_CITY", OLD_MAN2 }
return rows
end
-- The shared talk handler: TEXT_VIRIDIANCITY_OLD_MAN2's text_asm branch
-- (ViridianCityOldMan2Text) on EVENT_COMPLETED_CATCH_TRAINING.
local function oldMan2Talk(game, ow, npc, done)
if game.save.flags and game.save.flags.EVENT_COMPLETED_CATCH_TRAINING then
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game, text(game).losingMyTouch, done))
return
end
ow.runner:run(oldMan2Rows(game, ow, npc), { npc = npc, onDone = done })
end
M.VIRIDIAN_CITY = {
talk = {
TEXT_VIRIDIANCITY_OLD_MAN2 = oldMan2Talk,
},
-- Re-apply the Pokédex swap for a save that already holds the flag but
-- was never standing here when it fired (converted .sav imports, same
-- shape as story.lua's VIRIDIAN_CITY.onEnter). Yellow shows OLD_MAN2,
-- not the Red/Blue OLD_MAN at (17,5), and also puts away a stray
-- OLD_MAN a save made by the pre-#617 build left standing.
onEnter = function(game, ow)
if not (game.save.flags and game.save.flags.EVENT_GOT_POKEDEX) then
return
end
local Commands = require("src.script.Commands")
local ctx = { save = game.save, game = game, overworld = ow }
Commands.hide_object(ctx, "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN_SLEEPY")
Commands.hide_object(ctx, "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN")
Commands.show_object(ctx, "VIRIDIAN_CITY", OLD_MAN2)
end,
-- ViridianCityCheckWaitingOldMan: with the Pokédex held and the
-- tutorial undone, (19,9) -- the gap east of the old man, the same
-- cell the sleeper used to gate -- faces him right, turns the player
-- left and starts the OLD_MAN2 flow with no choice.
onStep = function(game, ow, x, y)
if baseViridianStep and baseViridianStep(game, ow, x, y) then
return true
end
local flags = game.save.flags
if not flags.EVENT_GOT_POKEDEX then return false end
if flags.EVENT_COMPLETED_CATCH_TRAINING then return false end
if x ~= 19 or y ~= 9 then return false end
local man
for _, npc in ipairs(ow.npcs) do
if npc.def and npc.def.name == OLD_MAN2 then man = npc break end
end
if not man then return false end
man.facing = "right"
ow.player.facing = "left"
oldMan2Talk(game, ow, man, nil)
return true
end,
}
return M
+7
View File
@@ -77,6 +77,13 @@ end
function Data:applyVersionedFieldData()
if require("src.core.GameVersion").isYellow() then
self.field.trades = copy(YELLOW_TRADES)
-- The old man's catch demo is a RATTATA in Yellow
-- (scripts/ViridianCity.asm ViridianCityOldManStartCatchTrainingScript
-- .SetupBattle: ld a, RATTATA / ld [wCurOpponent], a) but the Yellow
-- manifest inherited Red's WEEDLE field.oldManBattle (#617), so old
-- Yellow caches carry the wrong demo species too. The fixed import
-- manifest below stamps RATTATA for fresh imports.
self.field.oldManBattle = { species = "RATTATA", level = 5 }
end
end
+288
View File
@@ -0,0 +1,288 @@
-- Parity (#617): Yellow's Viridian old man is the OLD_MAN2 at (18,9),
-- not the Red/Blue OLD_MAN at (17,5), and his dialog has no yes/no
-- choice -- the apology speech runs the RATTATA demo battle straight
-- away, the post-battle line is the losing-my-touch text, and he walks
-- off and hides.
--
-- Oracle: pokeyellow scripts/OaksLab.asm (OaksLabOakGivesPokedexScript:
-- HideObject TOGGLE_LYING_OLD_MAN / ShowObject TOGGLE_OLD_MAN_2),
-- scripts/ViridianCity.asm (ViridianCityCheckWaitingOldMan,
-- ViridianCityOldMan2Text, ...InitialCatchTrainingScript,
-- ...PostInitialCatchTraining) and scripts/ViridianCity_2.asm
-- (ViridianCityPrintOldManText). The Red/Blue "Are you in a hurry?"
-- script was running against Yellow's text: YES printed the TimeIsMoney
-- alias (_ViridianCityOldManLosingMyTouchText) and NO ran the demo --
-- every talk, forever.
--
-- Self-contained: `luajit tests/parity_yellow_old_man.lua`; also globbed
-- by tests/run_tests.lua.
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.PALLET_TOWN) then Data:load() end
local GameVersion = require("src.core.GameVersion")
local SaveData = require("src.core.SaveData")
local ScriptRunner = require("src.script.ScriptRunner")
local TextBox = require("src.render.TextBox")
local BattleState = require("src.battle.BattleState")
local Pokemon = require("src.pokemon.Pokemon")
local S = require("tests.harness").suite("parity Yellow old man (#617)")
local check, eq = S.check, S.eq
local oldVersion = GameVersion.get()
local MAP = "VIRIDIAN_CITY"
local SLEEPER = "VIRIDIANCITY_OLD_MAN_SLEEPY"
local WALKER = "VIRIDIANCITY_OLD_MAN"
local OLD_MAN2 = "VIRIDIANCITY_OLD_MAN2"
local DONE_FLAG = "EVENT_COMPLETED_CATCH_TRAINING"
-- The Yellow wiring must be attached before anything else caches the
-- map-script registry: data.scripts.init branches on GameVersion at
-- load, so flip it first (this file owns its own process when run
-- standalone). Under tests/run_tests.lua the registry is already
-- cached with the Red wiring, so attach the Yellow modules directly
-- afterwards -- attachBase merges per TEXT constant and replaces hooks,
-- which is a no-op on a fresh process and the fix on a shared one.
GameVersion.set("yellow")
local mapScripts = require("data.scripts.init")
local MapScripts = require("src.script.MapScripts")
MapScripts.attachBase(MAP,
require("data.scripts.yellow_viridian_old_man").VIRIDIAN_CITY)
MapScripts.attachBase("OAKS_LAB",
require("data.scripts.oaks_lab_yellow"))
local oldManMod = require("data.scripts.yellow_viridian_old_man")
-- ------------------------------------------------------- the demo species
-- The catch demo is a RATTATA in Yellow (SetupBattle sets wCurOpponent
-- = RATTATA) but the Yellow manifest inherited Red's WEEDLE; the runtime
-- override in Data:applyVersionedFieldData repairs old caches. Kept
-- active until the end of this file so the demo-battle assertions below
-- run against the Yellow value; restored before S.finish() like
-- parity_yellow_trades does for its trades table.
local originalOldManBattle = Data.field.oldManBattle
or { species = "WEEDLE", level = 5 } -- the fixture carries no oldManBattle
local originalTrades = Data.field.trades
eq(originalOldManBattle.species, "WEEDLE",
"Red/Blue's old man still demos a Weedle")
GameVersion.set("yellow")
Data:applyVersionedFieldData()
eq(Data.field.oldManBattle.species, "RATTATA",
"Yellow's old man demos a Rattata (#617)")
local manifestFile = assert(io.open("tools/rom_manifest_yellow.json", "r"))
local yellowManifest = manifestFile:read("*a")
manifestFile:close()
check(yellowManifest:find('"species": "RATTATA"', 1, true) ~= nil,
"the Yellow manifest stamps RATTATA for fresh imports")
local redManifestFile = assert(io.open("tools/rom_manifest.json", "r"))
local redManifest = redManifestFile:read("*a")
redManifestFile:close()
check(redManifest:find('"species": "WEEDLE"', 1, true) ~= nil,
"and the Red/Blue manifest keeps WEEDLE")
-- ------------------------------------------------------- the Pokedex swap
-- OaksLabOakGivesPokedexScript shows TOGGLE_OLD_MAN_2 (the tutorial old
-- man standing on the sleeper's cell), never the Red/Blue walker
local oaksRows = mapScripts.talkScript("OAKS_LAB", "TEXT_OAKSLAB_OAK1")
check(type(oaksRows) == "table",
"the Yellow OaksLab Oak talk resolves to rows")
local sawSleepHide, sawOldMan2Show, sawOldManShow = false, false, false
for _, row in ipairs(oaksRows or {}) do
if row[1] == "hide_object" and row[3] == SLEEPER then sawSleepHide = true end
if row[1] == "show_object" and row[3] == OLD_MAN2 then sawOldMan2Show = true end
if row[1] == "show_object" and row[3] == WALKER then sawOldManShow = true end
end
check(sawSleepHide, "the Pokédex hand-over hides the lying old man")
check(sawOldMan2Show, "it shows OLD_MAN2 on the sleeper's cell")
check(not sawOldManShow, "it never shows the Red/Blue OLD_MAN (#617)")
-- both Yellow gamblers default hidden (toggle OFF), like pokeyellow
-- data/maps/toggleable_objects.asm. OLD_MAN2 only exists in a Yellow
-- import -- a Red-imported checkout carries just OLD_MAN -- so the
-- dataset checks tolerate its absence and the Yellow manifest carries
-- the OLD_MAN2 default instead.
local walkerDef, oldMan2Def
if Data.maps[MAP] then
for _, o in ipairs(Data.maps[MAP].objects or {}) do
if o.name == WALKER then walkerDef = o end
if o.name == OLD_MAN2 then oldMan2Def = o end
end
end
check(walkerDef == nil or walkerDef.hidden == true,
"VIRIDIANCITY_OLD_MAN defaults hidden in Yellow")
check(oldMan2Def == nil or oldMan2Def.hidden == true,
"VIRIDIANCITY_OLD_MAN2 defaults hidden in Yellow")
local om2Name = yellowManifest:find('"name": "VIRIDIANCITY_OLD_MAN2"', 1, true)
local om2Hidden = om2Name and yellowManifest:sub(
math.max(1, om2Name - 40), om2Name):find('"hidden": true', 1, true)
check(om2Hidden ~= nil,
"the Yellow manifest ships OLD_MAN2 with the toggle OFF")
-- ------------------------------------------------------- script registry
local talk = mapScripts.talkScript(MAP, "TEXT_VIRIDIANCITY_OLD_MAN2")
check(type(talk) == "function",
"TEXT_VIRIDIANCITY_OLD_MAN2 resolves to the Yellow handler")
check(type(mapScripts.talkScript(MAP, "TEXT_VIRIDIANCITY_OLD_MAN")) == "table",
"the Red/Blue OLD_MAN talk is still registered (unreachable in Yellow)")
local hooks = mapScripts.get(MAP)
check(hooks and type(hooks.onEnter) == "function",
"VIRIDIAN_CITY.onEnter is the Yellow swap")
check(hooks and type(hooks.onStep) == "function",
"VIRIDIAN_CITY.onStep chains the gym lock and sleeper gate")
check(oldManMod.VIRIDIAN_CITY and oldManMod.VIRIDIAN_CITY.talk
and oldManMod.VIRIDIAN_CITY.talk.TEXT_VIRIDIANCITY_OLD_MAN2 == talk,
"the handler is the module's own, not a leftover merge")
-- ------------------------------------------------------- completed branch
do
local pushed = {}
local game = {
data = Data,
save = SaveData.newGame(),
stack = { push = function(_, s) pushed[#pushed + 1] = s end },
}
game.save.flags.EVENT_COMPLETED_CATCH_TRAINING = true
local done = false
talk(game, nil, {}, function() done = true end)
eq(#pushed, 1, "a second talk only prints one box")
eq(getmetatable(pushed[1]), TextBox, "the losing-my-touch line, in a box")
pushed[1].onDone()
check(done, "closing it hands input back")
end
-- ------------------------------- the initial tutorial, end to end
-- Needs real species in the dataset (the fixture carries only FIX_*);
-- the engine's old-man demo machinery itself is parity_J's territory.
if Data.pokemon.RATTATA and Data.pokemon.PIKACHU then
do
require("src.render.Font").load(Data)
local pushed = {}
local save = SaveData.newGame()
save.party = { Pokemon.new(Data, "PIKACHU", 12) }
local moves = {}
local man = { def = { index = 8, name = OLD_MAN2 } }
local ow = {
map = { id = MAP, def = { label = "ViridianCity" } },
npcs = { man }, entities = { man },
player = { cellX = 19, cellY = 9, facing = "left" },
scriptMove = function(_, _, dir, _, cb) moves[#moves + 1] = dir; cb() end,
npcByIndex = function(_, i) if i == 8 then return man end end,
}
local game = {
data = Data,
save = save,
stack = { push = function(_, s) pushed[#pushed + 1] = s end },
}
local runner = ScriptRunner.new(game, ow)
ow.runner = runner
local done = false
talk(game, ow, man, function() done = true end)
eq(#pushed, 1, "the initial talk opens the apology speech")
eq(getmetatable(pushed[1]), TextBox, "in a text box")
pushed[1].onDone() -- A: the apology closes, the demo battle starts
eq(#pushed, 2, "the demo battle starts with no choice in between")
local battle = pushed[2]
check(battle and battle.demo, "it is the old-man demo battle")
eq(battle and battle.enemy and battle.enemy.mon.species, "RATTATA",
"the demo is a RATTATA in Yellow (#617)")
eq(save.flags[DONE_FLAG], nil, "the flag is still clear mid-demo")
battle.onFinish() -- the battle ends, the post-battle text prints
eq(save.flags[DONE_FLAG], true, "EVENT_COMPLETED_CATCH_TRAINING is set")
eq(#pushed, 3, "the losing-my-touch line follows the demo")
pushed[3].onDone() -- A: the old man walks off
eq(#moves, 6, "with the player on (19,9) he walks down 6 tiles")
check(moves[1] == "down" and moves[6] == "down",
"all six steps are the ViridianCityOldManMovementData2 walk")
eq(save.objectToggles[MAP] and save.objectToggles[MAP][OLD_MAN2], false,
"TOGGLE_OLD_MAN_2 hides once the walk finishes")
check(done, "and the talk hands input back")
end
-- ---------------------------------- side talk: player not on (19,9) cell
do
local pushed = {}
local save = SaveData.newGame()
save.party = { Pokemon.new(Data, "PIKACHU", 12) }
local moves = {}
local man = { def = { index = 8, name = OLD_MAN2 } }
local pika = { def = { index = 99, name = "PIKACHU_FOLLOWER" },
pikachuFollower = true }
local ow = {
map = { id = MAP, def = { label = "ViridianCity" } },
npcs = { man, pika }, entities = { man, pika },
player = { cellX = 18, cellY = 8, facing = "down" },
scriptMove = function(_, _, dir, _, cb) moves[#moves + 1] = dir; cb() end,
npcByIndex = function(_, i) if i == 8 then return man elseif i == 99 then return pika end end,
}
local game = {
data = Data,
save = save,
stack = { push = function(_, s) pushed[#pushed + 1] = s end },
}
local runner = ScriptRunner.new(game, ow)
ow.runner = runner
talk(game, ow, man, function() end)
pushed[1].onDone()
pushed[2].onFinish()
pushed[3].onDone()
eq(moves[1], "right", "Pikachu steps aside first (ViridianCityMovePikachu)")
eq(moves[2], "right", "then the old man turns right one tile")
eq(#moves, 2, "and no more")
end
else
check(true, "fixture dataset: demo-battle flow skipped (no RATTATA)")
end
-- --------------------------------------------------------- the (19,9) step
do
local pushed = {}
local save = SaveData.newGame()
local man = { def = { index = 8, name = OLD_MAN2 } }
local ow = {
map = { id = MAP, def = { label = "ViridianCity" } },
npcs = { man }, entities = { man },
player = { cellX = 19, cellY = 9, facing = "down" },
scriptMove = function(_, _, _, _, cb) cb() end,
npcByIndex = function() end,
}
local game = {
data = Data,
save = save,
stack = { push = function(_, s) pushed[#pushed + 1] = s end },
}
local runner = ScriptRunner.new(game, ow)
ow.runner = runner
check(not hooks.onStep(game, ow, 5, 5),
"off the trigger cell the step passes through")
check(hooks.onStep(game, ow, 19, 9),
"pre-Pokedex the sleeper gate owns (19,9)")
eq(#pushed, 1, "with the sleepy text box")
check(save.flags[DONE_FLAG] ~= true, "the tutorial is not running")
save.flags.EVENT_GOT_POKEDEX = true
check(hooks.onStep(game, ow, 19, 9),
"with the Pokedex, (19,9) starts the tutorial")
eq(man.facing, "right", "the old man faces the player")
eq(ow.player.facing, "left", "and the player turns to face him")
eq(#pushed, 2, "the apology box is up")
check(save.flags[DONE_FLAG] ~= true,
"no flag until the demo battle actually runs")
save.flags.EVENT_COMPLETED_CATCH_TRAINING = true
check(not hooks.onStep(game, ow, 19, 9),
"once the tutorial is done the cell is quiet again")
end
Data.field.trades = originalTrades
Data.field.oldManBattle = originalOldManBattle
GameVersion.set(oldVersion)
S.finish()
+1 -1
View File
@@ -6574,7 +6574,7 @@
"battleType": "BATTLE_TYPE_OLD_MAN",
"level": 5,
"map": "VIRIDIAN_CITY",
"species": "WEEDLE",
"species": "RATTATA",
"text": "ViridianCityOldManText"
},
"overworldFx": {