mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-13 00:40:59 +02:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fea81f03fc | |||
| b549e319c2 | |||
| 378246728b | |||
| f9adeca5ca | |||
| 838f87c224 | |||
| 5b519165fc | |||
| 1a2b23a066 | |||
| fe16b3259c | |||
| 2e46954fe0 | |||
| 00d13d3c35 | |||
| 1b8b3ad538 | |||
| 5f75cfd691 | |||
| 5f89def2ce | |||
| 7c26eb9a24 | |||
| d951fe8fc5 | |||
| ac01135ca8 | |||
| 5e89e35e02 | |||
| 7e7afeaf82 | |||
| c6fa6d294b | |||
| fc2d17fc78 | |||
| 9f072285a6 | |||
| c58ebe4b9b | |||
| 0dd187fe30 | |||
| ea78792c03 | |||
| 9954225701 | |||
| 6bb2e078c0 |
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
@@ -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 }}
|
||||
@@ -42,9 +42,15 @@ 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
|
||||
# directory; mask it first so absolute paths in every later step's
|
||||
# output show up as *** in the public workflow logs.
|
||||
- name: Mask runner paths
|
||||
run: echo "::add-mask::$HOME"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -121,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"
|
||||
@@ -170,14 +177,23 @@ jobs:
|
||||
set -euo pipefail
|
||||
scripts/build_android.sh --version "${{ steps.ver.outputs.version }}"
|
||||
|
||||
- name: Build iOS
|
||||
- name: Install xcbeautify
|
||||
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 }}"
|
||||
brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
|
||||
|
||||
- name: Build iOS
|
||||
env:
|
||||
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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: |
|
||||
@@ -187,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}"
|
||||
@@ -221,6 +238,7 @@ jobs:
|
||||
echo "Notarized + stapled ✓"
|
||||
|
||||
- name: Stage release assets
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
id: assets
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -260,6 +278,7 @@ jobs:
|
||||
cat "$outdir/sha256sums.txt"
|
||||
|
||||
- name: Publish GitHub Release
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
@@ -365,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
-- takes it ("I'll take this one, then!") and both balls disappear.
|
||||
-- Source: scripts/OaksLab.asm OaksLabCharmanderPokeBallText /
|
||||
-- OaksLabRivalTakePokeBallScript.
|
||||
-- * Leftover ball (after the pick): Oak turns and reads the last-mon
|
||||
-- line instead of re-offering the starter (OaksLabLastMonScript, #601).
|
||||
-- * Rival (object 1): before starter -> "go ahead and choose" once Oak
|
||||
-- has walked you in, else "gramps isn't around" (#218); with
|
||||
-- starter -> taunt + battle OPP_RIVAL1 with the counter-pick party
|
||||
@@ -23,7 +25,7 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
||||
{ "jump_if_true", 20 }, -- 2
|
||||
-- no picking until Oak has walked you in (OaksLabScript gating)
|
||||
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" }, -- 3
|
||||
{ "jump_if_false", 20 }, -- 4
|
||||
{ "jump_if_false", 22 }, -- 4
|
||||
-- the Pokédex "new species" entry shows before the ask (predef
|
||||
-- StarterDex ahead of OaksLabYouWant...Text). StarterDex temporarily
|
||||
-- sets the owned bits so ShowPokedexData prints height/weight/text;
|
||||
@@ -31,7 +33,7 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
||||
{ "push_screen", "DexEntryMenu",
|
||||
{ species = species, forceOwned = true } }, -- 5
|
||||
{ "ask", askText }, -- 6
|
||||
{ "jump_if_false", 21 }, -- 7
|
||||
{ "jump_if_false", "end" }, -- 7
|
||||
-- OaksLab.asm prints ReceivedMon then AddPartyMon (AskName lives
|
||||
-- inside give_pokemon). Show the received text first so the
|
||||
-- nickname prompt follows "you got X", matching Gen1.
|
||||
@@ -52,9 +54,16 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
||||
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
|
||||
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
|
||||
or "BULBASAUR" } }, -- 17
|
||||
{ "jump", 21 }, -- 18
|
||||
{ "jump", 21 }, -- 19 (spacer)
|
||||
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 20
|
||||
{ "jump", "end" }, -- 18
|
||||
{ "jump", "end" }, -- 19 (spacer)
|
||||
-- a leftover ball after the player's pick: Oak turns to face the
|
||||
-- player and reads the last-mon line instead of re-offering the
|
||||
-- starter (scripts/OaksLab.asm OaksLabSelectedPokeBallScript ->
|
||||
-- OaksLabLastMonScript; #601). The ROM's "#MON" ligature is spelled
|
||||
-- out as Pokémon here.
|
||||
{ "face_object", 5, "down" }, -- 20
|
||||
{ "show_text", "That's PROF.OAK's\nlast Pokémon!" }, -- 21
|
||||
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 22
|
||||
}
|
||||
end
|
||||
|
||||
@@ -245,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
|
||||
@@ -285,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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
@@ -1 +1 @@
|
||||
11.5
|
||||
12.0
|
||||
|
||||
+17
-38
@@ -1,4 +1,4 @@
|
||||
# iOS build (LÖVE 11.5)
|
||||
# iOS build (LÖVE 12.0)
|
||||
|
||||
> **Native ROM/mod/save import.** The iOS build ships a Swift
|
||||
> document-picker bridge (`native/GRPickerBridge.swift` + `GRBootstrap.m`)
|
||||
@@ -20,20 +20,17 @@
|
||||
> The note below about a missing "UIDocumentPicker handoff" is
|
||||
> resolved by this bridge.
|
||||
|
||||
macOS + Xcode only. Pins the official **LÖVE 11.5** iOS Xcode tree
|
||||
(`love-11.5-ios-source.zip` from [love2d/love releases](https://github.com/love2d/love/releases/tag/11.5)),
|
||||
matching `conf.lua`'s `t.version = "11.5"`.
|
||||
macOS + Xcode only. Fetches the **LÖVE 12.0** source tree and matching Apple
|
||||
dependencies from the official [LÖVE source](https://github.com/love2d/love)
|
||||
and [Apple dependencies](https://github.com/love2d/love-apple-dependencies)
|
||||
repositories. `conf.lua` declares LÖVE 12.0 on iOS and 11.5 elsewhere.
|
||||
|
||||
There is no separate `love2d/love-ios` GitHub repo for 11.5; the release zip
|
||||
**is** the vendored iOS project (Xcode project under
|
||||
`love-src/platform/xcode/love.xcodeproj`, target `love-ios`).
|
||||
|
||||
Pin file: [`LOVE_VERSION`](./LOVE_VERSION) → `11.5`.
|
||||
Pin file: [`LOVE_VERSION`](./LOVE_VERSION) → `12.0`.
|
||||
|
||||
## Quick start (simulator)
|
||||
|
||||
```bash
|
||||
# Fetch LÖVE 11.5 iOS sources (once) + build for Simulator
|
||||
# Fetch LÖVE 12.0 iOS sources and dependencies (once) + build for Simulator
|
||||
scripts/build_ios.sh --fetch
|
||||
```
|
||||
|
||||
@@ -80,10 +77,10 @@ Manual out-of-band steps:
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `LOVE_VERSION` | Engine pin (`11.5`) |
|
||||
| `LOVE_VERSION` | Engine pin (`12.0`) |
|
||||
| `overlays/love-ios.plist` | Portrait-only Info.plist + display name **Pokemon Red** (copied over the upstream plist every build) |
|
||||
| `love-src/` | Downloaded `love-11.5-ios-source` tree (**gitignored**, do not commit) |
|
||||
| `cache/` | Downloaded zips (**gitignored**) |
|
||||
| `love-src/` | Downloaded LÖVE 12.0 source tree (**gitignored**, do not commit) |
|
||||
| `cache/` | Temporary source and dependency checkout data (**gitignored**) |
|
||||
| `build/` | `xcodebuild` derived data (**gitignored**) |
|
||||
|
||||
Game payload lands at:
|
||||
@@ -94,28 +91,10 @@ and is fused into the built `.app` (LÖVE auto-runs any bundled `*.love`).
|
||||
|
||||
## Apple libraries dependency
|
||||
|
||||
The official `love-11.5-ios-source.zip` already ships prebuilt iOS
|
||||
xcframeworks under `platform/xcode/ios/libraries/` (SDL2, LuaJIT, freetype,
|
||||
ogg, vorbis, theora, modplug).
|
||||
|
||||
If that folder is missing or incomplete (e.g. you cloned sources without
|
||||
libs), download the matching prebuilts and install them:
|
||||
|
||||
```bash
|
||||
curl -fL -o mobile/ios/cache/love-11.5-apple-libraries.zip \
|
||||
https://github.com/love2d/love/releases/download/11.5/love-11.5-apple-libraries.zip
|
||||
unzip -q mobile/ios/cache/love-11.5-apple-libraries.zip -d mobile/ios/cache
|
||||
rm -rf mobile/ios/love-src/platform/xcode/ios/libraries
|
||||
cp -R mobile/ios/cache/love-apple-dependencies/iOS/libraries \
|
||||
mobile/ios/love-src/platform/xcode/ios/libraries
|
||||
```
|
||||
|
||||
`scripts/build_ios.sh` checks for `libraries/SDL2.xcframework` and fails with
|
||||
these instructions if it is absent.
|
||||
|
||||
Upstream also documents
|
||||
[love-apple-dependencies](https://github.com/love2d/love-apple-dependencies)
|
||||
as an alternate source of the same libraries.
|
||||
`scripts/build_ios.sh --fetch` retrieves the matching iOS libraries and the
|
||||
SDL3 framework from
|
||||
[love-apple-dependencies](https://github.com/love2d/love-apple-dependencies).
|
||||
Re-run it if either dependency directory is absent.
|
||||
|
||||
## App identity
|
||||
|
||||
@@ -134,7 +113,7 @@ so refreshing `love-src/` does not lose branding.
|
||||
| Flag | Meaning |
|
||||
|------|---------|
|
||||
| *(default)* | Simulator, Debug, no signing |
|
||||
| `--fetch` | Download/extract `love-11.5-ios-source.zip` if `love-src/` is missing |
|
||||
| `--fetch` | Fetch the LÖVE 12.0 source tree and Apple dependencies if `love-src/` is missing |
|
||||
| `--device` | Build against `iphoneos` instead of `iphonesimulator` |
|
||||
| `--release` | `Release` configuration instead of `Debug` |
|
||||
| `--package-only` | Zip `game.love` + apply plist overlay; skip `xcodebuild` |
|
||||
@@ -147,5 +126,5 @@ Also: `scripts/build.sh ios` delegates here (`--release` is forwarded).
|
||||
- iOS platform installed in Xcode (Settings → Platforms). `xcodebuild -showsdks`
|
||||
should list `iphonesimulator` / `iphoneos`. A partial install can fail IB/xib
|
||||
compiles with `iOS … Platform Not Installed` even when the SDK name appears.
|
||||
- `love-src/` present (`--fetch` or manual unzip of `love-11.5-ios-source.zip`)
|
||||
- iOS libraries under `love-src/platform/xcode/ios/libraries/` (see above)
|
||||
- `love-src/` present (`--fetch`)
|
||||
- iOS libraries under `love-src/platform/xcode/ios/libraries/` and SDL3 under `love-src/platform/xcode/shared/Frameworks/`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Applies gen1recomp's iOS native-bridge patches to the fetched LÖVE 11.5
|
||||
"""Applies gen1recomp's iOS native-bridge patches to the fetched LÖVE 12.0
|
||||
source tree (mobile/ios/love-src/). Idempotent AND re-appliable: the first
|
||||
run stashes a pristine `.orig` copy of every file it rewrites, and later
|
||||
runs always start over from that copy — so editing the patch content here
|
||||
@@ -39,6 +39,7 @@ WRAP_INCLUDES = """
|
||||
#ifdef LOVE_IOS
|
||||
#include <objc/runtime.h>
|
||||
#include <objc/message.h>
|
||||
#include <string>
|
||||
#include "filesystem/Filesystem.h"
|
||||
#endif
|
||||
""" % MARKER
|
||||
@@ -53,8 +54,12 @@ WRAP_FUNCS = """
|
||||
#ifdef LOVE_IOS
|
||||
static const char *gr_saveDirectory()
|
||||
{
|
||||
static std::string saveDirectory;
|
||||
auto fs = Module::getInstance<love::filesystem::Filesystem>(Module::M_FILESYSTEM);
|
||||
return fs != nullptr ? fs->getSaveDirectory() : "";
|
||||
if (fs == nullptr)
|
||||
return "";
|
||||
saveDirectory = fs->getSaveDirectory();
|
||||
return saveDirectory.c_str();
|
||||
}
|
||||
|
||||
static int gr_callBridge(lua_State *L, const char *className,
|
||||
|
||||
+46
-52
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Packages the LÖVE2D Pokémon Red port into an iOS app via LÖVE 11.5's
|
||||
# official iOS Xcode project (love-11.5-ios-source.zip).
|
||||
# Packages the LÖVE2D Pokémon Red port into an iOS app via LÖVE 12.0's
|
||||
# iOS Xcode project.
|
||||
#
|
||||
# Usage: scripts/build_ios.sh [--fetch] [--device] [--release] [--install]
|
||||
# [--version X.Y.Z] [--package-only]
|
||||
@@ -12,13 +12,12 @@
|
||||
# first connected iPhone/iPad (unlock it first)
|
||||
# --release Release configuration
|
||||
# --version X.Y.Z stamp MARKETING_VERSION / CURRENT_PROJECT_VERSION
|
||||
# --fetch Download love-11.5-ios-source.zip into mobile/ios/love-src/
|
||||
# --fetch Fetch LÖVE 12.0 sources and Apple dependencies into mobile/ios/love-src/
|
||||
# --package-only Zip game.love + apply plist overlay; skip xcodebuild
|
||||
#
|
||||
# Prerequisites:
|
||||
# - macOS + Xcode (xcodebuild)
|
||||
# - mobile/ios/love-src/ (see --fetch / mobile/ios/README.md)
|
||||
# - prebuilt iOS libraries under love-src/platform/xcode/ios/libraries/
|
||||
#
|
||||
# Output: dist/ios/<Config>-<sdk>/gen1recomp.app (convenience copy)
|
||||
# dist/ios/gen1recomp.ipa (device builds only)
|
||||
@@ -54,11 +53,11 @@ BUNDLE_ID="${GEN1_BUNDLE_ID:-com.theboisclub.gen1recomp}"
|
||||
if [ -z "$BUNDLE_ID" ] && [ -f "$IOS_DIR/bundle_id.local" ]; then
|
||||
BUNDLE_ID="$(tr -d '[:space:]' < "$IOS_DIR/bundle_id.local")"
|
||||
fi
|
||||
LOVE_VERSION="$(tr -d '[:space:]' < "$IOS_DIR/LOVE_VERSION" 2>/dev/null || echo 11.5)"
|
||||
IOS_SOURCE_ZIP="love-${LOVE_VERSION}-ios-source.zip"
|
||||
APPLE_LIBS_ZIP="love-${LOVE_VERSION}-apple-libraries.zip"
|
||||
IOS_SOURCE_URL="https://github.com/love2d/love/releases/download/${LOVE_VERSION}/${IOS_SOURCE_ZIP}"
|
||||
APPLE_LIBS_URL="https://github.com/love2d/love/releases/download/${LOVE_VERSION}/${APPLE_LIBS_ZIP}"
|
||||
LOVE_VERSION="$(tr -d '[:space:]' < "$IOS_DIR/LOVE_VERSION" 2>/dev/null || echo 12.0)"
|
||||
LOVE_SOURCE_REF="${LOVE_SOURCE_REF:-main}"
|
||||
APPLE_DEPENDENCIES_REF="${APPLE_DEPENDENCIES_REF:-main}"
|
||||
LOVE_SOURCE_REPO="https://github.com/love2d/love.git"
|
||||
APPLE_DEPENDENCIES_REPO="https://github.com/love2d/love-apple-dependencies.git"
|
||||
|
||||
FETCH=false
|
||||
DEVICE=false
|
||||
@@ -160,25 +159,19 @@ fi
|
||||
# --------------------------------------------------------------- fetch love-src
|
||||
fetch_love_ios() {
|
||||
mkdir -p "$CACHE"
|
||||
local zip_path="$CACHE/$IOS_SOURCE_ZIP"
|
||||
if [ ! -f "$zip_path" ]; then
|
||||
say "downloading $IOS_SOURCE_ZIP (LÖVE $LOVE_VERSION iOS sources)"
|
||||
curl -fL --progress-bar "$IOS_SOURCE_URL" -o "$zip_path" \
|
||||
|| fail "download failed: $IOS_SOURCE_URL"
|
||||
else
|
||||
say "using cached $zip_path"
|
||||
fi
|
||||
|
||||
say "extracting into $LOVE_SRC"
|
||||
rm -rf "$LOVE_SRC"
|
||||
local tmp
|
||||
tmp="$(mktemp -d "$CACHE/extract.XXXXXX")"
|
||||
unzip -q "$zip_path" -d "$tmp"
|
||||
# Zip root is love-<version>-ios-source/
|
||||
local extracted
|
||||
extracted="$(find "$tmp" -maxdepth 1 -mindepth 1 -type d ! -name '__MACOSX' | head -1)"
|
||||
[ -n "$extracted" ] || fail "unexpected layout inside $IOS_SOURCE_ZIP"
|
||||
mv "$extracted" "$LOVE_SRC"
|
||||
say "fetching LÖVE $LOVE_VERSION sources ($LOVE_SOURCE_REF)"
|
||||
git clone --depth 1 --branch "$LOVE_SOURCE_REF" "$LOVE_SOURCE_REPO" "$tmp/love" \
|
||||
|| fail "failed to fetch LÖVE sources from $LOVE_SOURCE_REPO"
|
||||
say "fetching Apple dependencies ($APPLE_DEPENDENCIES_REF)"
|
||||
git clone --depth 1 --branch "$APPLE_DEPENDENCIES_REF" "$APPLE_DEPENDENCIES_REPO" "$tmp/dependencies" \
|
||||
|| fail "failed to fetch Apple dependencies from $APPLE_DEPENDENCIES_REPO"
|
||||
rm -rf "$LOVE_SRC"
|
||||
mv "$tmp/love" "$LOVE_SRC"
|
||||
mkdir -p "$LIBS_DIR" "$XCODE_DIR/shared"
|
||||
cp -R "$tmp/dependencies/iOS/libraries/." "$LIBS_DIR"
|
||||
cp -R "$tmp/dependencies/shared/." "$XCODE_DIR/shared"
|
||||
rm -rf "$tmp"
|
||||
say "love-src ready (LÖVE $LOVE_VERSION)"
|
||||
}
|
||||
@@ -188,18 +181,12 @@ if [ ! -d "$XCODE_DIR/love.xcodeproj" ]; then
|
||||
fetch_love_ios
|
||||
else
|
||||
fail "LÖVE $LOVE_VERSION iOS sources not found at mobile/ios/love-src/.
|
||||
Fetch them (documented download of love-${LOVE_VERSION}-ios-source.zip):
|
||||
Fetch them:
|
||||
scripts/build_ios.sh --fetch
|
||||
Or manually:
|
||||
mkdir -p mobile/ios/cache
|
||||
curl -fL -o mobile/ios/cache/$IOS_SOURCE_ZIP \\
|
||||
$IOS_SOURCE_URL
|
||||
unzip -q mobile/ios/cache/$IOS_SOURCE_ZIP -d mobile/ios/cache
|
||||
mv mobile/ios/cache/love-${LOVE_VERSION}-ios-source mobile/ios/love-src
|
||||
See mobile/ios/README.md."
|
||||
fi
|
||||
elif $FETCH; then
|
||||
say "love-src already present; skipping download (delete mobile/ios/love-src to refresh)"
|
||||
say "love-src already present; skipping fetch (delete mobile/ios/love-src to refresh)"
|
||||
fi
|
||||
|
||||
[ -d "$XCODE_DIR/love.xcodeproj" ] \
|
||||
@@ -207,25 +194,16 @@ fi
|
||||
|
||||
# --------------------------------------------------------------- apple libraries
|
||||
require_ios_libraries() {
|
||||
if [ -d "$LIBS_DIR/SDL2.xcframework" ]; then
|
||||
if [ -d "$LIBS_DIR/SDL2.xcframework" ] && [ -d "$XCODE_DIR/shared/Frameworks/SDL3.xcframework" ]; then
|
||||
return 0
|
||||
fi
|
||||
fail "prebuilt iOS libraries missing at:
|
||||
$LIBS_DIR
|
||||
love-ios expects SDL2.xcframework (and friends) there.
|
||||
and shared/Frameworks.
|
||||
|
||||
The official love-${LOVE_VERSION}-ios-source.zip normally includes them.
|
||||
If they are absent, install love-${LOVE_VERSION}-apple-libraries.zip:
|
||||
Re-fetch the LÖVE $LOVE_VERSION source tree and its Apple dependencies:
|
||||
|
||||
mkdir -p mobile/ios/cache
|
||||
curl -fL -o mobile/ios/cache/$APPLE_LIBS_ZIP \\
|
||||
$APPLE_LIBS_URL
|
||||
unzip -q mobile/ios/cache/$APPLE_LIBS_ZIP -d mobile/ios/cache
|
||||
rm -rf mobile/ios/love-src/platform/xcode/ios/libraries
|
||||
cp -R mobile/ios/cache/love-apple-dependencies/iOS/libraries \\
|
||||
mobile/ios/love-src/platform/xcode/ios/libraries
|
||||
|
||||
See mobile/ios/README.md (Apple libraries dependency)."
|
||||
scripts/build_ios.sh --fetch"
|
||||
}
|
||||
|
||||
require_ios_libraries
|
||||
@@ -565,11 +543,27 @@ run_xcodebuild() {
|
||||
|
||||
say "xcodebuild love-ios ($config / $sdk)"
|
||||
set +e
|
||||
(
|
||||
cd "$XCODE_DIR"
|
||||
xcodebuild "${args[@]}"
|
||||
)
|
||||
local xc_status=$?
|
||||
local xc_status
|
||||
if command -v xcbeautify >/dev/null 2>&1; then
|
||||
(
|
||||
cd "$XCODE_DIR"
|
||||
xcodebuild "${args[@]}"
|
||||
) 2>&1 | xcbeautify
|
||||
local pipeline_status=("${PIPESTATUS[@]}")
|
||||
local xcode_status=${pipeline_status[0]}
|
||||
local beautify_status=${pipeline_status[1]}
|
||||
if [ "$xcode_status" -ne 0 ]; then
|
||||
xc_status=$xcode_status
|
||||
else
|
||||
xc_status=$beautify_status
|
||||
fi
|
||||
else
|
||||
(
|
||||
cd "$XCODE_DIR"
|
||||
xcodebuild "${args[@]}"
|
||||
)
|
||||
xc_status=$?
|
||||
fi
|
||||
set -e
|
||||
if [ "$xc_status" -ne 0 ]; then
|
||||
fail "xcodebuild failed (exit $xc_status).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -86,8 +86,58 @@ local function mergeConflictLists(conflicts, incompatible)
|
||||
return out
|
||||
end
|
||||
|
||||
-- Drop bytes that are not valid UTF-8 (malformed sequences, overlongs,
|
||||
-- surrogates, > U+10FFFF) and a leading BOM. LÖVE's text renderer raises
|
||||
-- "Invalid UTF-8" from love.graphics.print/printf, so any manifest string a
|
||||
-- panel may draw must be scrubbed here -- the one place every mod manifest
|
||||
-- passes through -- or a single mangled description crashes the whole MODS
|
||||
-- panel instead of misrendering one card.
|
||||
local function scrubUtf8(s)
|
||||
if type(s) ~= "string" then return s end
|
||||
s = s:gsub("^\239\187\191", "")
|
||||
local out, i, n = {}, 1, #s
|
||||
while i <= n do
|
||||
local b = s:byte(i)
|
||||
local len
|
||||
if b < 0x80 then len = 1
|
||||
elseif b >= 0xC2 and b <= 0xDF then len = 2
|
||||
elseif b >= 0xE0 and b <= 0xEF then len = 3
|
||||
elseif b >= 0xF0 and b <= 0xF4 then len = 4
|
||||
end
|
||||
local ok = len ~= nil and i + len - 1 <= n
|
||||
if ok and len > 1 then
|
||||
for j = i + 1, i + len - 1 do
|
||||
local c = s:byte(j)
|
||||
if c < 0x80 or c > 0xBF then ok = false; break end
|
||||
end
|
||||
if ok then
|
||||
-- boundary lead bytes narrow their second byte: no overlongs
|
||||
-- (E0/F0), no surrogates (ED), nothing past U+10FFFF (F4)
|
||||
local b2 = s:byte(i + 1)
|
||||
if (b == 0xE0 and b2 < 0xA0) or (b == 0xED and b2 > 0x9F)
|
||||
or (b == 0xF0 and b2 < 0x90) or (b == 0xF4 and b2 > 0x8F) then
|
||||
ok = false
|
||||
end
|
||||
end
|
||||
end
|
||||
if ok then
|
||||
out[#out + 1] = s:sub(i, i + len - 1)
|
||||
i = i + len
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
function Manifest.validate(raw, path)
|
||||
assert(type(raw) == "table", "manifest must be an object")
|
||||
-- scrubbed in place so every later reader agrees, including the launcher's
|
||||
-- badge derivation, which reads raw.category rather than the validated copy
|
||||
raw.name = scrubUtf8(raw.name)
|
||||
raw.version = scrubUtf8(raw.version)
|
||||
raw.description = scrubUtf8(raw.description)
|
||||
raw.category = scrubUtf8(raw.category)
|
||||
assert(type(raw.id) == "string" and raw.id:match("^[%w_%-]+$"),
|
||||
"manifest id must contain only letters, numbers, _ or -")
|
||||
assert(type(raw.name) == "string" and raw.name ~= "", "manifest name is required")
|
||||
|
||||
@@ -641,7 +641,8 @@ function Renderer:endFrame(zones, worldZones)
|
||||
-- runs, so dialogs, menus and the HUD sit on top as usual.
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.setScissor(0, 0, ww, wh)
|
||||
if love.system and love.system.getOS and love.system.getOS() == "iOS" then
|
||||
local loveMajor = love.getVersion()
|
||||
if love.system and love.system.getOS and love.system.getOS() == "iOS" and loveMajor >= 12 then
|
||||
love.graphics.draw(self.worldOverride, 0, wh, 0, 1 / dpiX, -1 / dpiY)
|
||||
else
|
||||
love.graphics.draw(self.worldOverride, 0, 0, 0, 1 / dpiX, 1 / dpiY)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
-- Driver: regression coverage for #601 "Wrong dialogue when interacting
|
||||
-- with Prof. Oak's last ball".
|
||||
--
|
||||
-- After the player picks a starter and the rival takes his, the leftover
|
||||
-- ball on the lab table must show "That's PROF.OAK's last Pokémon!" --
|
||||
-- pret/pokered scripts/OaksLab.asm OaksLabSelectedPokeBallScript jumps
|
||||
-- every ball handler to OaksLabLastMonScript once EVENT_GOT_STARTER is
|
||||
-- set (Oak turns to face the player first). The buggy port fell through
|
||||
-- to _OaksLabThoseArePokeBallsText ("Those are POKé BALLs...") instead.
|
||||
--
|
||||
-- Scenario A (the #601 regression): with a starter already picked, talk
|
||||
-- to the leftover ball -> Oak faces down, box says "last Pokémon!",
|
||||
-- and no starter offer/dex appears. Fails before the fix (the box
|
||||
-- says "Those are POKé BALLs").
|
||||
-- Scenario B (guard): with NO starter and not escorted in, the ball still
|
||||
-- says "Those are POKé BALLs". Passes before and after the fix.
|
||||
--
|
||||
-- Setup: flags are set directly (pick flow never runs), so all three
|
||||
-- balls stay visible; the player stands left of the Charmander ball
|
||||
-- (cell 6,3), the leftover slot for the Squirtle pick (rival took the
|
||||
-- Bulbasaur ball). The lab battle flag is set so the rival is gone and
|
||||
-- cannot intercept the talk. TextBox.new is hooked to capture the raw
|
||||
-- box text.
|
||||
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local origNew = TextBox.new
|
||||
local lastText
|
||||
TextBox.new = function(g, text, ...)
|
||||
lastText = text
|
||||
return origNew(g, text, ...)
|
||||
end
|
||||
|
||||
local function restore()
|
||||
TextBox.new = origNew
|
||||
end
|
||||
|
||||
local function setFlags(postPick)
|
||||
local flags = game.save.flags or {}
|
||||
game.save.flags = flags
|
||||
flags.EVENT_FOLLOWED_OAK_INTO_LAB = true
|
||||
if postPick then
|
||||
flags.EVENT_GOT_STARTER = true
|
||||
flags.EVENT_CHOSE_SQUIRTLE = true
|
||||
else
|
||||
flags.EVENT_GOT_STARTER = nil
|
||||
end
|
||||
-- rival already fought + gone, so he cannot intercept the talk
|
||||
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true
|
||||
end
|
||||
|
||||
-- Talk to the ball at cell 6,3 (Charmander slot): stand one cell left
|
||||
-- facing right and press A. Returns once a TextBox has been built.
|
||||
local function talkToBall()
|
||||
lastText = nil
|
||||
U.teleport(game, "OAKS_LAB", 5, 3, "right")
|
||||
U.wait(6)
|
||||
for _ = 1, 8 do
|
||||
U.tap(game, "a")
|
||||
for _ = 1, 30 do
|
||||
if lastText then return true end
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
return lastText ~= nil
|
||||
end
|
||||
|
||||
-- ---- Scenario A: leftover ball after the pick
|
||||
setFlags(true)
|
||||
local aBoxOpened = talkToBall()
|
||||
U.wait(30) -- let the typewriter reveal the line
|
||||
U.shot(game, DIR .. "/a_last_ball.png")
|
||||
local aText = lastText or "<none>"
|
||||
local aPass = aBoxOpened
|
||||
and aText:find("last Pokémon!", 1, true) ~= nil
|
||||
and aText:find("Those are", 1, true) == nil
|
||||
U.log("SCENARIO A box:", aText)
|
||||
U.log("SCENARIO A", aPass and "PASS" or "FAIL")
|
||||
|
||||
-- close the box
|
||||
for _ = 1, 10 do
|
||||
if game.stack:top() == game.overworld then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(2)
|
||||
end
|
||||
|
||||
-- ---- Scenario B: pre-escort ball text unchanged
|
||||
setFlags(false)
|
||||
local bBoxOpened = talkToBall()
|
||||
U.wait(30)
|
||||
U.shot(game, DIR .. "/b_pre_escort.png")
|
||||
local bText = lastText or "<none>"
|
||||
local bPass = bBoxOpened
|
||||
and bText:find("ThoseArePokeBalls", 1, true) ~= nil
|
||||
or bText:find("Those are", 1, true) ~= nil
|
||||
U.log("SCENARIO B box:", bText)
|
||||
U.log("SCENARIO B", bPass and "PASS" or "FAIL")
|
||||
|
||||
-- restore hooks before any assert so a failure can't leave them installed
|
||||
restore()
|
||||
|
||||
U.log("RESULT bug601", (aPass and bPass) and "PASS" or "FAIL")
|
||||
assert(aPass,
|
||||
"Leftover ball after the pick must say 'That's PROF.OAK's last "
|
||||
.. "Pokémon!' (no 'Those are POKé BALLs'); got: " .. aText)
|
||||
assert(bPass,
|
||||
"Pre-escort balls must keep the 'Those are POKé BALLs' line; got: "
|
||||
.. bText)
|
||||
end
|
||||
@@ -297,4 +297,35 @@ do
|
||||
eq(rows[1].name, "bare", "a nameless row falls back to its id")
|
||||
end
|
||||
|
||||
-- ------- manifest strings are scrubbed to valid UTF-8 (MODS panel crash:
|
||||
-- LÖVE's printf raises "Invalid UTF-8" on a mangled name/description, so
|
||||
-- validate must drop bad bytes before any panel draws them)
|
||||
|
||||
do
|
||||
local m = mf({ id = "utf", entry = "m.lua",
|
||||
-- BOM-prefixed name (a real manifest shipped this way), a Latin-1 e-acute
|
||||
-- (\233, invalid as UTF-8) in the description, and a lone continuation
|
||||
-- byte in the version
|
||||
name = "\239\187\191Run Mode",
|
||||
version = "1.0\128.0",
|
||||
description = "caf\233 latt\233",
|
||||
category = "UI\255" })
|
||||
eq(m.name, "Run Mode", "a leading BOM is stripped from the name")
|
||||
eq(m.version, "1.0.0", "invalid bytes are dropped from the version")
|
||||
eq(m.description, "caf latt", "Latin-1 bytes are dropped, not replaced")
|
||||
eq(m.raw.category, "UI", "raw.category is scrubbed in place for the badge")
|
||||
|
||||
local ok2 = mf({ id = "utf2", name = "Vers\195\163oVermelha", version = "1.0.0",
|
||||
entry = "m.lua", description = "Pok\195\169mon \240\159\148\165" })
|
||||
eq(ok2.name, "Vers\195\163oVermelha", "valid two-byte sequences survive")
|
||||
eq(ok2.description, "Pok\195\169mon \240\159\148\165",
|
||||
"valid three- and four-byte sequences survive")
|
||||
|
||||
-- surrogate half (ED A0 80) and overlong slash (C0 AF) are invalid even
|
||||
-- though their lead bytes look plausible
|
||||
local bad = mf({ id = "utf3", name = "a\237\160\128b\192\175c",
|
||||
version = "1.0.0", entry = "m.lua" })
|
||||
eq(bad.name, "abc", "surrogates and overlongs are dropped")
|
||||
end
|
||||
|
||||
T.finish("launcher_mods")
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
-- Regression coverage for #601 "Wrong dialogue when interacting with Prof.
|
||||
-- Oak's last ball" (T2, ROM-free).
|
||||
--
|
||||
-- pret/pokered scripts/OaksLab.asm OaksLabSelectedPokeBallScript: once
|
||||
-- EVENT_GOT_STARTER is set, EVERY ball's text handler jumps to
|
||||
-- OaksLabLastMonScript -- Oak turns to face the player and reads
|
||||
-- "_OaksLabLastMonText" ("That's PROF.OAK's last #MON!") instead of
|
||||
-- re-offering the starter. The buggy port fell through to
|
||||
-- _OaksLabThoseArePokeBallsText ("Those are POKé BALLs...") on every ball
|
||||
-- once a starter had been picked. The fix also spells the ROM's "#MON"
|
||||
-- ligature out as "Pokémon".
|
||||
--
|
||||
-- The three ball scripts share one table (starterBall), so this suite
|
||||
-- drives that table through a mini ScriptRunner-compatible executor:
|
||||
-- flag checks, jumps, "end" halts and text rows are executed, UI-heavy
|
||||
-- commands (push_screen, ask, give_pokemon, npc moves) are no-ops with
|
||||
-- ask recording the offer text. It then asserts the whole flow:
|
||||
-- * GOT_STARTER + talk -> Oak faces down + "last Pokémon!" line, ends
|
||||
-- * no GOT_STARTER, not escorted in -> "Those are POKé BALLs"
|
||||
-- * no GOT_STARTER, escorted in -> the dex/ask offer (unchanged path)
|
||||
-- plus MapScripts.validateContribution stays clean (the pre-fix table
|
||||
-- carried nine out-of-range "jump 21" findings -- its run-time "end").
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local MapScripts = require("src.script.MapScripts")
|
||||
|
||||
local contribution = dofile("data/scripts/oaks_lab.lua")
|
||||
local problems = MapScripts.validateContribution(contribution)
|
||||
T.eq(#problems, 0, "oaks_lab contribution validates cleanly")
|
||||
for _, p in ipairs(problems) do
|
||||
T.check(false, "unexpected finding: " .. p)
|
||||
end
|
||||
|
||||
local BALL = "TEXT_OAKSLAB_CHARMANDER_POKE_BALL"
|
||||
|
||||
-- ---- mini executor over the talk rows (ScriptRunner semantics: a jump
|
||||
-- command returns the next row index or "end" to halt)
|
||||
|
||||
local function run(script, flags, answer)
|
||||
local pc, texts, offers = 1, {}, {}
|
||||
local lastCheck = nil
|
||||
while pc <= #script do
|
||||
local row = script[pc]
|
||||
local verb = row[1]
|
||||
if verb == "check_flag" then
|
||||
lastCheck = flags[row[2]] == true
|
||||
elseif verb == "jump_if_true" then
|
||||
if lastCheck then
|
||||
if row[2] == "end" then break end
|
||||
pc = row[2] goto next
|
||||
end
|
||||
elseif verb == "jump_if_false" then
|
||||
if not lastCheck then
|
||||
if row[2] == "end" then break end
|
||||
pc = row[2] goto next
|
||||
end
|
||||
elseif verb == "jump" then
|
||||
if row[2] == "end" then break end
|
||||
pc = row[2]
|
||||
goto next
|
||||
elseif verb == "show_text" then
|
||||
texts[#texts + 1] = row[2]
|
||||
elseif verb == "ask" then
|
||||
offers[#offers + 1] = row[2]
|
||||
if answer == false then
|
||||
pc = pc + 1 -- decline: the next row's jump_if_false decides
|
||||
goto next
|
||||
end
|
||||
end
|
||||
-- push_screen / give_pokemon / set_flag / hide_object / move_npc_to /
|
||||
-- face_object: no-op here (set_flag is exercised via the fixture
|
||||
-- flags table instead of being run)
|
||||
pc = pc + 1
|
||||
::next::
|
||||
end
|
||||
return texts, offers
|
||||
end
|
||||
|
||||
local function concat(list)
|
||||
return table.concat(list, "\n")
|
||||
end
|
||||
|
||||
-- ---- leftover ball after the pick: Oak faces down + the last-mon line
|
||||
local got = { EVENT_GOT_STARTER = true, EVENT_FOLLOWED_OAK_INTO_LAB = true }
|
||||
local texts, offers = run(contribution.talk[BALL], got, true)
|
||||
T.eq(#offers, 0, "no starter offer after the pick")
|
||||
local box = concat(texts)
|
||||
T.check(box:find("last Pokémon!", 1, true) ~= nil,
|
||||
"leftover ball says the last-mon line (got: " .. box .. ")")
|
||||
T.check(box:find("Those are", 1, true) == nil,
|
||||
"leftover ball no longer says 'Those are POKé BALLs'")
|
||||
T.check(box:find("#MON", 1, true) == nil,
|
||||
"the ROM #MON ligature is spelled out as Pokémon")
|
||||
|
||||
-- the pokered beat also turns Oak to face the player
|
||||
T.check(contribution.talk[BALL][20][1] == "face_object"
|
||||
and contribution.talk[BALL][20][2] == 5
|
||||
and contribution.talk[BALL][20][3] == "down",
|
||||
"row 20 faces Oak down before the line (OaksLabLastMonScript)")
|
||||
|
||||
-- ---- pre-escort: still the vanilla "Those are POKé BALLs" line
|
||||
local pre = { EVENT_GOT_STARTER = false, EVENT_FOLLOWED_OAK_INTO_LAB = false }
|
||||
local t2, o2 = run(contribution.talk[BALL], pre, true)
|
||||
T.eq(#o2, 0, "no offer before Oak escorts the player in")
|
||||
T.check(concat(t2):find("ThoseArePokeBalls", 1, true) ~= nil,
|
||||
"pre-escort balls keep the 'Those are POKé BALLs' line")
|
||||
|
||||
-- ---- escorted in but no pick yet: the dex + "You want X?" offer
|
||||
local mid = { EVENT_GOT_STARTER = false, EVENT_FOLLOWED_OAK_INTO_LAB = true }
|
||||
local t3, o3 = run(contribution.talk[BALL], mid, true)
|
||||
T.eq(#o3, 1, "the starter offer still runs before the pick")
|
||||
T.check(concat(t3):find("last Pokémon!", 1, true) == nil,
|
||||
"no last-mon line before the pick")
|
||||
|
||||
-- ---- all three balls share the same table shape (last-mon beat present)
|
||||
for _, key in ipairs({
|
||||
"TEXT_OAKSLAB_CHARMANDER_POKE_BALL",
|
||||
"TEXT_OAKSLAB_SQUIRTLE_POKE_BALL",
|
||||
"TEXT_OAKSLAB_BULBASAUR_POKE_BALL",
|
||||
}) do
|
||||
local script = contribution.talk[key]
|
||||
T.check(script and script[21] and script[21][2] and
|
||||
script[21][2]:find("Pokémon", 1, true) ~= nil,
|
||||
key .. " carries the last-mon line")
|
||||
end
|
||||
|
||||
T.finish("oaks_lab_last_ball_bug601")
|
||||
@@ -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()
|
||||
@@ -6574,7 +6574,7 @@
|
||||
"battleType": "BATTLE_TYPE_OLD_MAN",
|
||||
"level": 5,
|
||||
"map": "VIRIDIAN_CITY",
|
||||
"species": "WEEDLE",
|
||||
"species": "RATTATA",
|
||||
"text": "ViridianCityOldManText"
|
||||
},
|
||||
"overworldFx": {
|
||||
|
||||
Reference in New Issue
Block a user