mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 11:11:10 +02:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bd28390ea | |||
| fa886899fc | |||
| a81126a03a | |||
| 164c555bb4 | |||
| e5926893a9 | |||
| dbcaf705c2 | |||
| f6809be81d | |||
| 3ddf70888e | |||
| fe7dcf33ec | |||
| 733450bf86 | |||
| 1dae9622e1 | |||
| da0fa5c9ad | |||
| e576dea676 | |||
| 4cac51a831 | |||
| fef844e220 | |||
| 0f45bb5792 | |||
| b6a397460e |
@@ -7,6 +7,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -28,6 +29,13 @@ jobs:
|
|||||||
[ -n "$pr_number" ] || exit 0
|
[ -n "$pr_number" ] || exit 0
|
||||||
echo "artifact_url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$artifact_id" >> "$GITHUB_OUTPUT"
|
echo "artifact_url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$artifact_id" >> "$GITHUB_OUTPUT"
|
||||||
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
|
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Delete existing comment
|
||||||
|
if: steps.artifact.outputs.pr_number != ''
|
||||||
|
uses: izhangzhihao/delete-comment@master
|
||||||
|
with:
|
||||||
|
github_token: ${{ github.token }}
|
||||||
|
delete_user_name: github-actions[bot]
|
||||||
|
issue_number: ${{ steps.artifact.outputs.pr_number }}
|
||||||
- name: Get build info
|
- name: Get build info
|
||||||
id: build-info
|
id: build-info
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -397,12 +397,18 @@ jobs:
|
|||||||
date="$(date -u +"%Y-%m-%d")"
|
date="$(date -u +"%Y-%m-%d")"
|
||||||
size="$(wc -c < "$ipa" | tr -d '[:space:]')"
|
size="$(wc -c < "$ipa" | tr -d '[:space:]')"
|
||||||
download_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/v${v}/gen1recomp-${v}-ios.ipa"
|
download_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/v${v}/gen1recomp-${v}-ios.ipa"
|
||||||
|
localized_description="Gen1Recomp - A native Lua / LÖVE2D recreation of Gen 1 Poke"
|
||||||
|
release_notes="$(GH_TOKEN="${{ github.token }}" gh release view "v${v}" --json body --jq '.body // ""' 2>/dev/null || true)"
|
||||||
|
if [ -n "$release_notes" ]; then
|
||||||
|
localized_description="$release_notes"
|
||||||
|
fi
|
||||||
entry="$(jq -n \
|
entry="$(jq -n \
|
||||||
--arg version "$v" \
|
--arg version "$v" \
|
||||||
--arg date "$date" \
|
--arg date "$date" \
|
||||||
--arg download_url "$download_url" \
|
--arg download_url "$download_url" \
|
||||||
|
--arg localized_description "$localized_description" \
|
||||||
--argjson size "$size" \
|
--argjson size "$size" \
|
||||||
'{version: $version, date: $date, size: $size, downloadURL: $download_url, localizedDescription: "Gen1Recomp - A native Lua / LÖVE2D recreation of Gen 1 Poke"}')"
|
'{version: $version, date: $date, size: $size, downloadURL: $download_url, localizedDescription: $localized_description}')"
|
||||||
|
|
||||||
if jq -e --arg version "$v" \
|
if jq -e --arg version "$v" \
|
||||||
'any(.apps[] | select(.bundleIdentifier == "com.theboisclub.gen1recomp").versions[]?; .version == $version)' \
|
'any(.apps[] | select(.bundleIdentifier == "com.theboisclub.gen1recomp").versions[]?; .version == $version)' \
|
||||||
@@ -417,15 +423,33 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
mv "$app_repo.tmp" "$app_repo"
|
mv "$app_repo.tmp" "$app_repo"
|
||||||
|
|
||||||
|
# main is PR-only for everyone except deploy keys (the "main protection"
|
||||||
|
# ruleset's bypass actor), so this push must authenticate with the
|
||||||
|
# RELEASE_DEPLOY_KEY deploy key over SSH; the workflow's GITHUB_TOKEN
|
||||||
|
# would be rejected by the branch protection.
|
||||||
- name: Commit iOS app repository
|
- name: Commit iOS app repository
|
||||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||||
uses: EndBug/add-and-commit@v10
|
env:
|
||||||
with:
|
DEPLOY_KEY: ${{ secrets.RELEASE_DEPLOY_KEY }}
|
||||||
add: mobile/ios/app-repo.json
|
run: |
|
||||||
default_author: github_actions
|
set -euo pipefail
|
||||||
message: "chore(ios): update app-repo.json [skip ci]"
|
git add mobile/ios/app-repo.json
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "app-repo.json unchanged; nothing to push"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
key="$RUNNER_TEMP/release-deploy-key"
|
||||||
|
printf '%s\n' "$DEPLOY_KEY" > "$key"
|
||||||
|
chmod 600 "$key"
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git commit -m "chore(ios): update app-repo.json [skip ci]"
|
||||||
|
git -c core.sshCommand="ssh -i $key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \
|
||||||
|
push "git@github.com:${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_REF_NAME}"
|
||||||
|
rm -f "$key"
|
||||||
|
|
||||||
- name: Clean up signing keychain
|
- name: Clean up signing keychain
|
||||||
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
|
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
|
||||||
run: |
|
run: |
|
||||||
security delete-keychain "$RUNNER_TEMP/pokemon-signing.keychain-db" 2>/dev/null || true
|
security delete-keychain "$RUNNER_TEMP/pokemon-signing.keychain-db" 2>/dev/null || true
|
||||||
|
rm -f "$RUNNER_TEMP/release-deploy-key"
|
||||||
|
|||||||
+23
-18
@@ -22,10 +22,10 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
|||||||
rivalBallX, rivalBall)
|
rivalBallX, rivalBall)
|
||||||
return {
|
return {
|
||||||
{ "check_flag", "EVENT_GOT_STARTER" }, -- 1
|
{ "check_flag", "EVENT_GOT_STARTER" }, -- 1
|
||||||
{ "jump_if_true", 20 }, -- 2
|
{ "jump_if_true", 22 }, -- 2
|
||||||
-- no picking until Oak has walked you in (OaksLabScript gating)
|
-- no picking until Oak has walked you in (OaksLabScript gating)
|
||||||
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" }, -- 3
|
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" }, -- 3
|
||||||
{ "jump_if_false", 23 }, -- 4
|
{ "jump_if_false", 25 }, -- 4
|
||||||
-- the Pokédex "new species" entry shows before the ask (predef
|
-- the Pokédex "new species" entry shows before the ask (predef
|
||||||
-- StarterDex ahead of OaksLabYouWant...Text). StarterDex temporarily
|
-- StarterDex ahead of OaksLabYouWant...Text). StarterDex temporarily
|
||||||
-- sets the owned bits so ShowPokedexData prints height/weight/text;
|
-- sets the owned bits so ShowPokedexData prints height/weight/text;
|
||||||
@@ -37,36 +37,41 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
|||||||
-- OaksLab.asm prints ReceivedMon then AddPartyMon (AskName lives
|
-- OaksLab.asm prints ReceivedMon then AddPartyMon (AskName lives
|
||||||
-- inside give_pokemon). Show the received text first so the
|
-- inside give_pokemon). Show the received text first so the
|
||||||
-- nickname prompt follows "you got X", matching Gen1.
|
-- nickname prompt follows "you got X", matching Gen1.
|
||||||
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 8
|
-- The received text carries sound_get_key_item (OaksLab.asm
|
||||||
{ "give_pokemon", species, 5 }, -- 9
|
-- OaksLabReceivedMonText); the jingle plays as the box opens
|
||||||
{ "set_flag", "EVENT_GOT_STARTER" }, -- 10
|
-- (same beat as the Yellow port's starter, #668).
|
||||||
{ "set_flag", choseFlag }, -- 11
|
{ "play_sound", "Get_Key_Item" }, -- 8
|
||||||
|
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 9
|
||||||
|
{ "give_pokemon", species, 5 }, -- 10
|
||||||
|
{ "set_flag", "EVENT_GOT_STARTER" }, -- 11
|
||||||
|
{ "set_flag", choseFlag }, -- 12
|
||||||
-- POKé BALLs are not handed out here in the original -- Oak gives
|
-- POKé BALLs are not handed out here in the original -- Oak gives
|
||||||
-- them later, at OaksLabOak1Text's .give_poke_balls beat once the
|
-- them later, at OaksLabOak1Text's .give_poke_balls beat once the
|
||||||
-- player has beaten the Route 22 rival (see TEXT_OAKSLAB_OAK1 below)
|
-- player has beaten the Route 22 rival (see TEXT_OAKSLAB_OAK1 below)
|
||||||
{ "hide_object", "OAKS_LAB", ownBall }, -- 12
|
{ "hide_object", "OAKS_LAB", ownBall }, -- 13
|
||||||
-- the rival walks to the countering ball (around the furniture)
|
-- the rival walks to the countering ball (around the furniture)
|
||||||
{ "move_npc_to", 1, rivalBallX, 4 }, -- 13
|
{ "move_npc_to", 1, rivalBallX, 4 }, -- 14
|
||||||
{ "face_object", 1, "up" }, -- 14
|
{ "face_object", 1, "up" }, -- 15
|
||||||
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 15
|
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 16
|
||||||
{ "hide_object", "OAKS_LAB", rivalBall }, -- 16
|
{ "hide_object", "OAKS_LAB", rivalBall }, -- 17
|
||||||
|
{ "play_sound", "Get_Key_Item" }, -- 18 (sound_get_key_item)
|
||||||
{ "show_text", "_OaksLabRivalReceivedMonText",
|
{ "show_text", "_OaksLabRivalReceivedMonText",
|
||||||
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
|
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
|
||||||
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
|
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
|
||||||
or "BULBASAUR" } }, -- 17
|
or "BULBASAUR" } }, -- 19
|
||||||
{ "jump", "end" }, -- 18
|
{ "jump", "end" }, -- 20
|
||||||
{ "jump", "end" }, -- 19 (spacer)
|
{ "jump", "end" }, -- 21 (spacer)
|
||||||
-- a leftover ball after the player's pick: Oak turns to face the
|
-- 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
|
-- player and reads the last-mon line instead of re-offering the
|
||||||
-- starter (scripts/OaksLab.asm OaksLabSelectedPokeBallScript ->
|
-- starter (scripts/OaksLab.asm OaksLabSelectedPokeBallScript ->
|
||||||
-- OaksLabLastMonScript; #601). The ROM's "#MON" ligature is spelled
|
-- OaksLabLastMonScript; #601). The ROM's "#MON" ligature is spelled
|
||||||
-- out as Pokémon here.
|
-- out as Pokémon here.
|
||||||
{ "face_object", 5, "down" }, -- 20
|
{ "face_object", 5, "down" }, -- 22
|
||||||
{ "show_text", "That's PROF.OAK's\nlast Pokémon!" }, -- 21
|
{ "show_text", "That's PROF.OAK's\nlast Pokémon!" }, -- 23
|
||||||
-- OaksLabLastMonScript ends at TextScriptEnd; the port used to fall
|
-- OaksLabLastMonScript ends at TextScriptEnd; the port used to fall
|
||||||
-- through into the pre-pick line below (#601 remnant, reported on #600)
|
-- through into the pre-pick line below (#601 remnant, reported on #600)
|
||||||
{ "jump", "end" }, -- 22
|
{ "jump", "end" }, -- 24
|
||||||
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 23
|
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 25
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,15 @@
|
|||||||
"localizedDescription": "Gen1Recomp - A native Lua / LÖVE2D recreation of Gen 1 Poke",
|
"localizedDescription": "Gen1Recomp - A native Lua / LÖVE2D recreation of Gen 1 Poke",
|
||||||
"tintColor": "3b5ca8",
|
"tintColor": "3b5ca8",
|
||||||
"category": "games",
|
"category": "games",
|
||||||
"versions": []
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "0.1.58",
|
||||||
|
"date": "2026-08-02",
|
||||||
|
"size": 8331163,
|
||||||
|
"downloadURL": "https://github.com/bryanthaboi/gen1recomp/releases/download/v0.1.58/gen1recomp-0.1.58-ios.ipa",
|
||||||
|
"localizedDescription": "Gen1Recomp - A native Lua / LÖVE2D recreation of Gen 1 Poke"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-12
@@ -343,23 +343,12 @@ pack_game_love() {
|
|||||||
done
|
done
|
||||||
say "game.love: $(du -h "$LOVE_FILE" | cut -f1) -> $LOVE_FILE"
|
say "game.love: $(du -h "$LOVE_FILE" | cut -f1) -> $LOVE_FILE"
|
||||||
|
|
||||||
# This script packs its own game.love (it does not reuse build.sh's), so it
|
|
||||||
# stamps the release version the same way build.sh and build_android.sh do:
|
|
||||||
# patch a copy of Version.lua (engine set to $VERSION) under a throwaway
|
|
||||||
# staging dir and replace the entry inside the archive in place -- never the
|
|
||||||
# source tree. Stamping the Info.plist alone is not enough: the mod loader
|
|
||||||
# reads Version.engine out of game.love (src/mods/Loader.lua game_version
|
|
||||||
# gate), so an unstamped archive reports "0.0.0-dev" and every mod with a
|
|
||||||
# version floor is rejected on iOS while it loads on desktop (#613).
|
|
||||||
# VERSION is already validated as X.Y.Z above; when it is empty the packaged
|
|
||||||
# game keeps the "0.0.0-dev" default so a dev build cannot pass for a
|
|
||||||
# release. The stamp is read back out and the build fails if it did not take.
|
|
||||||
if printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
if printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||||
say "stamping engine version $VERSION into game.love"
|
say "stamping engine version $VERSION into game.love"
|
||||||
local stamp_dir
|
local stamp_dir
|
||||||
stamp_dir="$(mktemp -d)"
|
stamp_dir="$(mktemp -d)"
|
||||||
mkdir -p "$stamp_dir/src/core"
|
mkdir -p "$stamp_dir/src/core"
|
||||||
sed -E "s/(engine[[:space:]]*=[[:space:]]*\")[^\"]*(\")/\1$VERSION\2/" \
|
sed -E "s/(engine[[:space:]]*=[[:space:]]*\")([^\"]*)(\")/\1$VERSION\3/" \
|
||||||
"$ROOT/src/core/Version.lua" > "$stamp_dir/src/core/Version.lua"
|
"$ROOT/src/core/Version.lua" > "$stamp_dir/src/core/Version.lua"
|
||||||
(cd "$stamp_dir" && zip -q "$LOVE_FILE" src/core/Version.lua)
|
(cd "$stamp_dir" && zip -q "$LOVE_FILE" src/core/Version.lua)
|
||||||
local version_re
|
local version_re
|
||||||
@@ -464,6 +453,86 @@ print("patched project.pbxproj")
|
|||||||
PY
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suppress_love_dependency_warnings() {
|
||||||
|
local liblove_pbx="$XCODE_DIR/liblove.xcodeproj/project.pbxproj"
|
||||||
|
local love_pbx="$XCODE_DIR/love.xcodeproj/project.pbxproj"
|
||||||
|
[ -f "$liblove_pbx" ] || fail "missing $liblove_pbx"
|
||||||
|
[ -f "$love_pbx" ] || fail "missing $love_pbx"
|
||||||
|
|
||||||
|
python3 - "$liblove_pbx" "$love_pbx" <<'PY'
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def patch_configs(path, config_ids, settings):
|
||||||
|
text = path.read_text()
|
||||||
|
for config_id in config_ids:
|
||||||
|
marker = f"\t\t{config_id}"
|
||||||
|
start = text.find(marker)
|
||||||
|
if start < 0:
|
||||||
|
raise SystemExit(f"missing configuration {config_id}")
|
||||||
|
settings_start = text.find("\t\t\tbuildSettings = {\n", start)
|
||||||
|
block_end = text.find("\n\t\t};", settings_start)
|
||||||
|
if settings_start < 0 or block_end < 0:
|
||||||
|
raise SystemExit(f"invalid configuration {config_id}")
|
||||||
|
block = text[settings_start:block_end]
|
||||||
|
lines = block.splitlines(keepends=True)
|
||||||
|
for setting in settings:
|
||||||
|
key = setting.split(" = ", 1)[0].strip()
|
||||||
|
prefix = f"{key} ="
|
||||||
|
replaced = False
|
||||||
|
normalized = []
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith(f"\t\t\t\t{prefix}"):
|
||||||
|
if not replaced:
|
||||||
|
normalized.append(setting)
|
||||||
|
replaced = True
|
||||||
|
else:
|
||||||
|
normalized.append(line)
|
||||||
|
if not replaced:
|
||||||
|
normalized.insert(1, setting)
|
||||||
|
lines = normalized
|
||||||
|
normalized_block = "".join(lines)
|
||||||
|
if normalized_block != block:
|
||||||
|
text = text[:settings_start] + normalized_block + text[block_end:]
|
||||||
|
path.write_text(text)
|
||||||
|
|
||||||
|
patch_configs(
|
||||||
|
pathlib.Path(sys.argv[1]),
|
||||||
|
(
|
||||||
|
"FA0B78EF1A958B90000E1D17",
|
||||||
|
"FA0B78F01A958B90000E1D17",
|
||||||
|
"FA0B78F11A958B90000E1D17",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"\t\t\t\tCLANG_WARN_UNINITIALIZED_AUTOS = NO;\n",
|
||||||
|
"\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = NO;\n",
|
||||||
|
"\t\t\t\tCLANG_WARN_UNUSED_PARAMETER = NO;\n",
|
||||||
|
"\t\t\t\tGCC_WARN_CHECK_SWITCH_STATEMENTS = NO;\n",
|
||||||
|
"\t\t\t\tGCC_WARN_SIGN_COMPARE = NO;\n",
|
||||||
|
"\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = NO;\n",
|
||||||
|
"\t\t\t\tGCC_WARN_UNUSED_FUNCTION = NO;\n",
|
||||||
|
"\t\t\t\tGCC_WARN_UNUSED_PARAMETER = NO;\n",
|
||||||
|
"\t\t\t\tGCC_WARN_UNUSED_VARIABLE = NO;\n",
|
||||||
|
"\t\t\t\tOTHER_CFLAGS = \"$(inherited) -Wno-sign-compare -Wno-strict-prototypes -Wno-unused-but-set-variable -Wno-unused-function -Wno-unused-parameter -Wno-unused-variable\";\n",
|
||||||
|
"\t\t\t\tOTHER_CPLUSPLUSFLAGS = \"$(inherited) -Wno-deprecated-declarations -Wno-non-c-typedef-for-linkage -Wno-sign-compare -Wno-switch -Wno-unguarded-availability-new -Wno-unused-but-set-variable -Wno-unused-function -Wno-unused-parameter -Wno-unused-private-field -Wno-unused-variable\";\n",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
patch_configs(
|
||||||
|
pathlib.Path(sys.argv[2]),
|
||||||
|
(
|
||||||
|
"FA0B7F261A95AAF4000E1D17",
|
||||||
|
"FA0B7F271A95AAF4000E1D17",
|
||||||
|
"FA0B7F281A95AAF4000E1D17",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"\t\t\t\tCLANG_WARN_UNDECLARED_SELECTOR = NO;\n",
|
||||||
|
"\t\t\t\tCLANG_WARN_UNUSED_PARAMETER = NO;\n",
|
||||||
|
"\t\t\t\tOTHER_CFLAGS = \"$(inherited) -Wno-undeclared-selector -Wno-unused-parameter\";\n",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
# --------------------------------------------------------------- xcodebuild
|
# --------------------------------------------------------------- xcodebuild
|
||||||
# love.system.pickFile and createFile are a native bridge compiled in by
|
# love.system.pickFile and createFile are a native bridge compiled in by
|
||||||
# mobile/ios/patch_love_src.py, not part of LÖVE. A build that skipped the
|
# mobile/ios/patch_love_src.py, not part of LÖVE. A build that skipped the
|
||||||
@@ -543,6 +612,7 @@ run_xcodebuild() {
|
|||||||
MARKETING_VERSION="$marketing_version"
|
MARKETING_VERSION="$marketing_version"
|
||||||
CURRENT_PROJECT_VERSION="$project_version"
|
CURRENT_PROJECT_VERSION="$project_version"
|
||||||
ONLY_ACTIVE_ARCH=NO
|
ONLY_ACTIVE_ARCH=NO
|
||||||
|
DISABLE_MANUAL_TARGET_ORDER_BUILD_WARNING=YES
|
||||||
)
|
)
|
||||||
|
|
||||||
if ! $DEVICE; then
|
if ! $DEVICE; then
|
||||||
@@ -702,6 +772,7 @@ python3 "$IOS_DIR/patch_love_src.py" || fail "patch_love_src.py failed"
|
|||||||
ensure_manifests
|
ensure_manifests
|
||||||
pack_game_love
|
pack_game_love
|
||||||
ensure_game_love_in_xcode
|
ensure_game_love_in_xcode
|
||||||
|
suppress_love_dependency_warnings
|
||||||
|
|
||||||
if $PACKAGE_ONLY; then
|
if $PACKAGE_ONLY; then
|
||||||
say "package-only: skipping xcodebuild (game.love + plist ready under mobile/ios/love-src/)"
|
say "package-only: skipping xcodebuild (game.love + plist ready under mobile/ios/love-src/)"
|
||||||
|
|||||||
+112
-49
@@ -33,6 +33,65 @@ local WideBattle = require("src.battle.WideBattle")
|
|||||||
local BattleState = {}
|
local BattleState = {}
|
||||||
BattleState.__index = BattleState
|
BattleState.__index = BattleState
|
||||||
BattleState.isOpaque = true
|
BattleState.isOpaque = true
|
||||||
|
|
||||||
|
-- pokered prints the battle lines itself (engine/battle/core.asm and the
|
||||||
|
-- move-effect banks), and the importer extracts every one of them, so the
|
||||||
|
-- port paraphrasing them in Lua meant the screen showed a near-miss of the
|
||||||
|
-- game's own wording while the cache held the real line -- and on a
|
||||||
|
-- localized import it showed English over translated data.
|
||||||
|
--
|
||||||
|
-- fromRom prefers the extracted text and keeps the literal as the catalog
|
||||||
|
-- fallback, for a cache built before the label and for the pure-module
|
||||||
|
-- tests that run without a dataset. The battle text's slots ({USER},
|
||||||
|
-- {TARGET}, the {RAM:...} buffers) are NOT in the token registry that
|
||||||
|
-- TextBox.substitute serves -- it only resolves {PLAYER}, {RIVAL} and
|
||||||
|
-- three string buffers -- so they are spliced here, in argument order,
|
||||||
|
-- before the box ever sees the string. {PLAYER}/{RIVAL} are left alone
|
||||||
|
-- for that later pass.
|
||||||
|
-- {PLAYER}/{RIVAL} are the two slots TextBox.substitute can fill on its
|
||||||
|
-- own, so they are only consumed here when the caller clearly supplies
|
||||||
|
-- them: an argument count matching every slot. Matching just the other
|
||||||
|
-- slots leaves those two for the later pass. Anything else means the
|
||||||
|
-- extracted line cannot carry what the call has to say -- a few labels
|
||||||
|
-- stop at a dynamic marker the decoder does not follow, e.g.
|
||||||
|
-- _EnemysWeakText extracts as "The enemy's weak!\nGet'm! " with nowhere
|
||||||
|
-- to put the name -- so the engine's own wording stands in rather than
|
||||||
|
-- printing a sentence with a hole in it.
|
||||||
|
local function fromRom(data, label, fallback, ...)
|
||||||
|
local text = data and data.text and data.text[label]
|
||||||
|
if not text then return Strings(fallback, ...) end
|
||||||
|
local args = { ... }
|
||||||
|
if #args == 0 then return text end
|
||||||
|
|
||||||
|
local slots, named = 0, 0
|
||||||
|
for token in text:gmatch("%b{}") do
|
||||||
|
slots = slots + 1
|
||||||
|
if token == "{PLAYER}" or token == "{RIVAL}" then named = named + 1 end
|
||||||
|
end
|
||||||
|
local fillNamed
|
||||||
|
if #args == slots then
|
||||||
|
fillNamed = true
|
||||||
|
elseif #args == slots - named then
|
||||||
|
fillNamed = false
|
||||||
|
else
|
||||||
|
return Strings(fallback, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
local index = 0
|
||||||
|
return (text:gsub("%b{}", function(token)
|
||||||
|
if not fillNamed and (token == "{PLAYER}" or token == "{RIVAL}") then
|
||||||
|
return token
|
||||||
|
end
|
||||||
|
index = index + 1
|
||||||
|
local value = args[index]
|
||||||
|
if value == nil then return token end
|
||||||
|
return tostring(value)
|
||||||
|
end))
|
||||||
|
end
|
||||||
|
|
||||||
|
function BattleState:romText(label, fallback, ...)
|
||||||
|
return fromRom(self.data, label, fallback, ...)
|
||||||
|
end
|
||||||
-- Letterbox voids around the 160x144 battle canvas fill white so the
|
-- Letterbox voids around the 160x144 battle canvas fill white so the
|
||||||
-- window reads as one continuous battle screen (no black bars).
|
-- window reads as one continuous battle screen (no black bars).
|
||||||
BattleState.letterboxWhite = true
|
BattleState.letterboxWhite = true
|
||||||
@@ -569,9 +628,9 @@ function BattleState.newWild(game, species, level, opts)
|
|||||||
self.enemy = makeBattler(game.data, Pokemon.new(game.data, species, level), false)
|
self.enemy = makeBattler(game.data, Pokemon.new(game.data, species, level), false)
|
||||||
markSeen(game, species)
|
markSeen(game, species)
|
||||||
if opts and opts.hooked then
|
if opts and opts.hooked then
|
||||||
self.introText = Strings("The hooked\n%s\nattacked!", self.enemy.name)
|
self.introText = self:romText("_HookedMonAttackedText", "The hooked\n%s\nattacked!", self.enemy.name)
|
||||||
else
|
else
|
||||||
self.introText = Strings("Wild %s\nappeared!", self.enemy.name)
|
self.introText = self:romText("_WildMonAppearedText", "Wild %s\nappeared!", self.enemy.name)
|
||||||
end
|
end
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
@@ -753,7 +812,7 @@ function BattleState:queueScopeReveal()
|
|||||||
or Strings("SILPH SCOPE\nunveiled the\vGHOST's identity!"))
|
or Strings("SILPH SCOPE\nunveiled the\vGHOST's identity!"))
|
||||||
self:act(function() self.ghostReveal = { t = 0 } end)
|
self:act(function() self.ghostReveal = { t = 0 } end)
|
||||||
table.insert(self.queue, { wait = BattleState.GHOST_REVEAL_FRAMES })
|
table.insert(self.queue, { wait = BattleState.GHOST_REVEAL_FRAMES })
|
||||||
self:say(Strings("Wild %s\nappeared!",
|
self:say(self:romText("_WildMonAppearedText", "Wild %s\nappeared!",
|
||||||
self.ghostReal and self.ghostReal.name or self.enemy.name))
|
self.ghostReal and self.ghostReal.name or self.enemy.name))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1292,7 +1351,7 @@ function BattleState:sendOutText(name)
|
|||||||
if pct >= 70 then return Strings("Go! %s!", name) end
|
if pct >= 70 then return Strings("Go! %s!", name) end
|
||||||
if pct >= 40 then return Strings("Do it! %s!", name) end
|
if pct >= 40 then return Strings("Do it! %s!", name) end
|
||||||
if pct >= 10 then return Strings("Get'm! %s!", name) end
|
if pct >= 10 then return Strings("Get'm! %s!", name) end
|
||||||
return Strings("The enemy's weak!\nGet'm! %s!", name)
|
return self:romText("_EnemysWeakText", "The enemy's weak!\nGet'm! %s!", name)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- audio/play_battle_music.asm: gym leaders (wGymLeaderNo) get the
|
-- audio/play_battle_music.asm: gym leaders (wGymLeaderNo) get the
|
||||||
@@ -1866,11 +1925,11 @@ function BattleState:update(dt)
|
|||||||
end
|
end
|
||||||
local mv = moves[self.moveIndex]
|
local mv = moves[self.moveIndex]
|
||||||
if self.player.disabledSlot == self.moveIndex then
|
if self.player.disabledSlot == self.moveIndex then
|
||||||
self:say(Strings("The move is\ndisabled!"))
|
self:say(self:romText("_MoveDisabledText", "The move is\ndisabled!"))
|
||||||
self.phase = "messages"
|
self.phase = "messages"
|
||||||
self.afterQueue = "menu"
|
self.afterQueue = "menu"
|
||||||
elseif mv.pp <= 0 then
|
elseif mv.pp <= 0 then
|
||||||
self:say(Strings("No PP left for\nthis move!"))
|
self:say(self:romText("_MoveNoPPText", "No PP left for\nthis move!"))
|
||||||
self.phase = "messages"
|
self.phase = "messages"
|
||||||
self.afterQueue = "menu"
|
self.afterQueue = "menu"
|
||||||
else
|
else
|
||||||
@@ -1920,7 +1979,7 @@ function BattleState:resolveMimic(user, target, move, moveInst)
|
|||||||
table.insert(self.queue, self.nextInsert, { wait = 50 })
|
table.insert(self.queue, self.nextInsert, { wait = 50 })
|
||||||
if target.invulnerable
|
if target.invulnerable
|
||||||
or not self:accuracyRoll(move, user, target) then
|
or not self:accuracyRoll(move, user, target) then
|
||||||
self:sayNext(Strings("But, it failed!"))
|
self:sayNext(self:romText("_ButItFailedText", "But, it failed!"))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local slots = {}
|
local slots = {}
|
||||||
@@ -1930,7 +1989,7 @@ function BattleState:resolveMimic(user, target, move, moveInst)
|
|||||||
if #slots == 0 then
|
if #slots == 0 then
|
||||||
-- .getRandomMove rerolls empty slots forever; a moveless target
|
-- .getRandomMove rerolls empty slots forever; a moveless target
|
||||||
-- can't happen in practice, so just fail instead of hanging
|
-- can't happen in practice, so just fail instead of hanging
|
||||||
self:sayNext(Strings("But, it failed!"))
|
self:sayNext(self:romText("_ButItFailedText", "But, it failed!"))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if user.isPlayer and self.kind ~= "link" then
|
if user.isPlayer and self.kind ~= "link" then
|
||||||
@@ -1991,7 +2050,7 @@ function BattleState:applyMimic(user, target, moveInst, slot)
|
|||||||
entry.mimic = true
|
entry.mimic = true
|
||||||
self:animNext("MIMIC", user.isPlayer)
|
self:animNext("MIMIC", user.isPlayer)
|
||||||
-- _MimicLearnedMoveText: "<USER> / learned / MOVE!"
|
-- _MimicLearnedMoveText: "<USER> / learned / MOVE!"
|
||||||
self:sayNext(Strings("%s\nlearned\n%s!", displayName(user),
|
self:sayNext(self:romText("_MimicLearnedMoveText", "%s\nlearned\n%s!", displayName(user),
|
||||||
self.data.moves[src.id].name))
|
self.data.moves[src.id].name))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3053,7 +3112,7 @@ function BattleState:executeAction(user, target, action)
|
|||||||
-- 3392): sleep/freeze/held/flinch keep the mon recharging next turn
|
-- 3392): sleep/freeze/held/flinch keep the mon recharging next turn
|
||||||
if self:preRechargeChecks(user, target) then return end
|
if self:preRechargeChecks(user, target) then return end
|
||||||
user.mustRecharge = nil
|
user.mustRecharge = nil
|
||||||
self:sayNext(Strings("%s\nmust recharge!", displayName(user)))
|
self:sayNext(self:romText("_MustRechargeText", "%s\nmust recharge!", displayName(user)))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if action.special == "bound" then
|
if action.special == "bound" then
|
||||||
@@ -3098,8 +3157,8 @@ function BattleState:statusOnomatopoeia(user, kind)
|
|||||||
anim = isPlayer and "CONF_PLAYER_ANIM" or "CONF_ANIM"
|
anim = isPlayer and "CONF_PLAYER_ANIM" or "CONF_ANIM"
|
||||||
end
|
end
|
||||||
local text = kind == "sleep"
|
local text = kind == "sleep"
|
||||||
and Strings("%s\nis fast asleep!", displayName(user))
|
and self:romText("_FastAsleepText", "%s\nis fast asleep!", displayName(user))
|
||||||
or Strings("%s\nis confused!", displayName(user))
|
or self:romText("_IsConfusedText", "%s\nis confused!", displayName(user))
|
||||||
if kind == "sleep" and isPlayer then
|
if kind == "sleep" and isPlayer then
|
||||||
self:animNext(anim, isPlayer)
|
self:animNext(anim, isPlayer)
|
||||||
self:sayNext(text)
|
self:sayNext(text)
|
||||||
@@ -3138,18 +3197,18 @@ function BattleState:preRechargeChecks(user, target)
|
|||||||
user.sleepTurns = (user.sleepTurns or 1) - 1
|
user.sleepTurns = (user.sleepTurns or 1) - 1
|
||||||
if user.sleepTurns <= 0 then
|
if user.sleepTurns <= 0 then
|
||||||
mon.status = nil
|
mon.status = nil
|
||||||
self:sayNext(Strings("%s\nwoke up!", displayName(user)))
|
self:sayNext(self:romText("_WokeUpText", "%s\nwoke up!", displayName(user)))
|
||||||
else
|
else
|
||||||
self:statusOnomatopoeia(user, "sleep")
|
self:statusOnomatopoeia(user, "sleep")
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if mon.status == "FRZ" then
|
if mon.status == "FRZ" then
|
||||||
self:sayNext(Strings("%s\nis frozen solid!", displayName(user)))
|
self:sayNext(self:romText("_IsFrozenText", "%s\nis frozen solid!", displayName(user)))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if target.trappingTurns then
|
if target.trappingTurns then
|
||||||
self:sayNext(Strings("%s\ncan't move!", displayName(user)))
|
self:sayNext(self:romText("_CantMoveText", "%s\ncan't move!", displayName(user)))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if user.flinched then
|
if user.flinched then
|
||||||
@@ -3157,7 +3216,7 @@ function BattleState:preRechargeChecks(user, target)
|
|||||||
-- player recharges, so the flinch eats the recharge turn and the
|
-- player recharges, so the flinch eats the recharge turn and the
|
||||||
-- flag survives (the Hyper Beam flinch glitch)
|
-- flag survives (the Hyper Beam flinch glitch)
|
||||||
user.flinched = false
|
user.flinched = false
|
||||||
self:sayNext(Strings("%s\nflinched!", displayName(user)))
|
self:sayNext(self:romText("_FlinchedText", "%s\nflinched!", displayName(user)))
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
@@ -3178,7 +3237,7 @@ function BattleState:statusInterrupt(user, target)
|
|||||||
{ id = "CONFUSED", power = 40, type = "NORMAL", accuracy = 100 },
|
{ id = "CONFUSED", power = 40, type = "NORMAL", accuracy = 100 },
|
||||||
{ rng = self.rng, forceCrit = false, typeless = true,
|
{ rng = self.rng, forceCrit = false, typeless = true,
|
||||||
screens = target })
|
screens = target })
|
||||||
self:sayNext(Strings("It hurt itself in\nits confusion!"))
|
self:sayNext(self:romText("_HurtItselfText", "It hurt itself in\nits confusion!"))
|
||||||
self:clearVolatiles(user, true)
|
self:clearVolatiles(user, true)
|
||||||
self:applyDamage(user, dmg)
|
self:applyDamage(user, dmg)
|
||||||
if user.mon.hp <= 0 then self:onFaint(user) end
|
if user.mon.hp <= 0 then self:onFaint(user) end
|
||||||
@@ -3266,7 +3325,7 @@ function BattleState:performMove(user, target, moveInst, isCalled)
|
|||||||
|
|
||||||
self.moveAnimRow = nil
|
self.moveAnimRow = nil
|
||||||
if not (user.thrashTurns and moveInst == user.thrashMove and user.thrashAnnounced) then
|
if not (user.thrashTurns and moveInst == user.thrashMove and user.thrashAnnounced) then
|
||||||
self:sayNext(Strings("%s\nused %s!", displayName(user), move.name))
|
self:sayNext(self:romText("_ItemUseText001", "%s\nused %s!", displayName(user), move.name))
|
||||||
-- the move's animation plays right after the announcement; the
|
-- the move's animation plays right after the announcement; the
|
||||||
-- damage path attaches the target's hit blink to this row so the
|
-- damage path attaches the target's hit blink to this row so the
|
||||||
-- blink follows the animation (pokered's order). Mimic is the
|
-- blink follows the animation (pokered's order). Mimic is the
|
||||||
@@ -3351,7 +3410,7 @@ function BattleState:performMove(user, target, moveInst, isCalled)
|
|||||||
-- SleepEffect/PoisonEffect/... call PlayCurrentMoveAnimation only
|
-- SleepEffect/PoisonEffect/... call PlayCurrentMoveAnimation only
|
||||||
-- after the effect lands; a miss skips it
|
-- after the effect lands; a miss skips it
|
||||||
self:cancelMoveAnim()
|
self:cancelMoveAnim()
|
||||||
self:sayNext(Strings("%s's\nattack missed!", displayName(user)))
|
self:sayNext(self:romText("_AttackMissedText", "%s's\nattack missed!", displayName(user)))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local msgs = record.run(ctx)
|
local msgs = record.run(ctx)
|
||||||
@@ -3371,7 +3430,7 @@ function BattleState:performMove(user, target, moveInst, isCalled)
|
|||||||
if move.power == 0 and not (record and record.kind == "full") then
|
if move.power == 0 and not (record and record.kind == "full") then
|
||||||
MoveEffects.warnUnknown(move.effect)
|
MoveEffects.warnUnknown(move.effect)
|
||||||
self:cancelMoveAnim()
|
self:cancelMoveAnim()
|
||||||
self:sayNext(Strings("But, it failed!"))
|
self:sayNext(self:romText("_ButItFailedText", "But, it failed!"))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -3380,7 +3439,7 @@ function BattleState:performMove(user, target, moveInst, isCalled)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function BattleState:continueTrapping(user, target)
|
function BattleState:continueTrapping(user, target)
|
||||||
self:sayNext(Strings("%s's\nattack continues!", displayName(user)))
|
self:sayNext(self:romText("_AttackContinuesText", "%s's\nattack continues!", displayName(user)))
|
||||||
-- .MultiturnMoveCheck (core.asm:3554-3566) prints AttackContinuesText
|
-- .MultiturnMoveCheck (core.asm:3554-3566) prints AttackContinuesText
|
||||||
-- then jumps to GetPlayerAnimationType, so the trapping move's full
|
-- then jumps to GetPlayerAnimationType, so the trapping move's full
|
||||||
-- animation replays each locked turn (same damage, animation shown).
|
-- animation replays each locked turn (same damage, animation shown).
|
||||||
@@ -3406,12 +3465,12 @@ function BattleState:continueBide(user, target)
|
|||||||
self:sayNext(Strings("%s\nis storing energy!", displayName(user)))
|
self:sayNext(Strings("%s\nis storing energy!", displayName(user)))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self:sayNext(Strings("%s\nunleashed energy!", displayName(user)))
|
self:sayNext(self:romText("_UnleashedEnergyText", "%s\nunleashed energy!", displayName(user)))
|
||||||
local dmg = (user.bideDamage or 0) * 2
|
local dmg = (user.bideDamage or 0) * 2
|
||||||
user.bideTurns, user.bideDamage = nil, nil
|
user.bideTurns, user.bideDamage = nil, nil
|
||||||
if dmg <= 0 then
|
if dmg <= 0 then
|
||||||
self:cancelMoveAnim()
|
self:cancelMoveAnim()
|
||||||
self:sayNext(Strings("But, it failed!"))
|
self:sayNext(self:romText("_ButItFailedText", "But, it failed!"))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- .UnleashEnergy (core.asm:3501-3529) re-points wPlayerMoveNum at BIDE
|
-- .UnleashEnergy (core.asm:3501-3529) re-points wPlayerMoveNum at BIDE
|
||||||
@@ -3434,9 +3493,9 @@ function BattleState:applyDamage(target, dmg)
|
|||||||
target.substituteHP = target.substituteHP - dmg
|
target.substituteHP = target.substituteHP - dmg
|
||||||
if target.substituteHP <= 0 then
|
if target.substituteHP <= 0 then
|
||||||
target.substituteHP = nil
|
target.substituteHP = nil
|
||||||
self:sayNext(Strings("%s's\nSUBSTITUTE broke!", displayName(target)))
|
self:sayNext(self:romText("_SubstituteBrokeText", "%s's\nSUBSTITUTE broke!", displayName(target)))
|
||||||
else
|
else
|
||||||
self:sayNext(Strings("The SUBSTITUTE\ntook damage for\n%s!", displayName(target)))
|
self:sayNext(self:romText("_SubstituteTookDamageText", "The SUBSTITUTE\ntook damage for\n%s!", displayName(target)))
|
||||||
end
|
end
|
||||||
return dmg
|
return dmg
|
||||||
end
|
end
|
||||||
@@ -3448,7 +3507,7 @@ function BattleState:applyDamage(target, dmg)
|
|||||||
end
|
end
|
||||||
if target.rageMove and dealt > 0 then
|
if target.rageMove and dealt > 0 then
|
||||||
target.stages.attack = math.min(6, (target.stages.attack or 0) + 1)
|
target.stages.attack = math.min(6, (target.stages.attack or 0) + 1)
|
||||||
self:sayNext(Strings("%s's\nRAGE is building!", displayName(target)))
|
self:sayNext(self:romText("_BuildingRageText", "%s's\nRAGE is building!", displayName(target)))
|
||||||
end
|
end
|
||||||
return dealt
|
return dealt
|
||||||
end
|
end
|
||||||
@@ -3776,7 +3835,7 @@ function BattleState:enemyMonFainted()
|
|||||||
-- scripted battles that print their own follow-up leave it nil.
|
-- scripted battles that print their own follow-up leave it nil.
|
||||||
self:actNext(function() self:playVictoryMusic() end)
|
self:actNext(function() self:playVictoryMusic() end)
|
||||||
-- _TrainerDefeatedText: "<PLAYER> defeated\nTRAINER!"
|
-- _TrainerDefeatedText: "<PLAYER> defeated\nTRAINER!"
|
||||||
self:sayNext(Strings("%s defeated\n%s!", self.game.save.player.name,
|
self:sayNext(self:romText("_TrainerDefeatedText", "%s defeated\n%s!", self.game.save.player.name,
|
||||||
self.trainer.name))
|
self.trainer.name))
|
||||||
self:actNext(function()
|
self:actNext(function()
|
||||||
self.showEnemyTrainer = self.trainerPic ~= nil
|
self.showEnemyTrainer = self.trainerPic ~= nil
|
||||||
@@ -3803,7 +3862,7 @@ function BattleState:enemyMonFainted()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self:sayNext(Strings("%s got ¥%d\nfor winning!", self.game.save.player.name, prize))
|
self:sayNext(self:romText("_MoneyForWinningText", "%s got ¥%d\nfor winning!", self.game.save.player.name, prize))
|
||||||
end
|
end
|
||||||
self.result = "win"
|
self.result = "win"
|
||||||
self.afterQueue = "finish"
|
self.afterQueue = "finish"
|
||||||
@@ -3819,7 +3878,7 @@ function BattleState:learnMove(mon, moveId)
|
|||||||
if #mon.moves < 4 then
|
if #mon.moves < 4 then
|
||||||
table.insert(mon.moves, { id = moveId, pp = mdef.pp })
|
table.insert(mon.moves, { id = moveId, pp = mdef.pp })
|
||||||
Runtime.emit("pokemon.move_learned", { mon = mon, moveId = moveId })
|
Runtime.emit("pokemon.move_learned", { mon = mon, moveId = moveId })
|
||||||
self:sayNext(Strings("%s learned\n%s!", mon.nickname or self.data.pokemon[mon.species].name,
|
self:sayNext(self:romText("_MimicLearnedMoveText", "%s learned\n%s!", mon.nickname or self.data.pokemon[mon.species].name,
|
||||||
mdef.name))
|
mdef.name))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -3901,11 +3960,11 @@ function BattleState:playerMonFainted()
|
|||||||
local pSpd = (game.save.party[1].stats or { speed = 0 }).speed or 0
|
local pSpd = (game.save.party[1].stats or { speed = 0 }).speed or 0
|
||||||
if self:runRoll(pSpd, TurnOrder.effectiveSpeed(self.enemy)) then
|
if self:runRoll(pSpd, TurnOrder.effectiveSpeed(self.enemy)) then
|
||||||
require("src.core.Sound").play(self.data, "Run")
|
require("src.core.Sound").play(self.data, "Run")
|
||||||
self:say(Strings("Got away safely!"))
|
self:say(self:romText("_GotAwayText", "Got away safely!"))
|
||||||
self.result = "run"
|
self.result = "run"
|
||||||
self.afterQueue = "finish"
|
self.afterQueue = "finish"
|
||||||
else
|
else
|
||||||
self:say(Strings("Can't escape!"))
|
self:say(self:romText("_CantEscapeText", "Can't escape!"))
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
@@ -3925,7 +3984,7 @@ function BattleState:openReplacementMenu()
|
|||||||
forceSwitch = true,
|
forceSwitch = true,
|
||||||
onSwitch = function(mon)
|
onSwitch = function(mon)
|
||||||
if mon.hp <= 0 then
|
if mon.hp <= 0 then
|
||||||
self:say(Strings("There's no will\nto fight!"))
|
self:say(self:romText("_NoWillText", "There's no will\nto fight!"))
|
||||||
return -- the menu-phase guard reopens the menu
|
return -- the menu-phase guard reopens the menu
|
||||||
end
|
end
|
||||||
self:restoreMimicked(self.player)
|
self:restoreMimicked(self.player)
|
||||||
@@ -3969,7 +4028,7 @@ function BattleState:safariAction(choice)
|
|||||||
|
|
||||||
if choice == "run" then
|
if choice == "run" then
|
||||||
require("src.core.Sound").play(self.data, "Run")
|
require("src.core.Sound").play(self.data, "Run")
|
||||||
self:say(Strings("Got away safely!"))
|
self:say(self:romText("_GotAwayText", "Got away safely!"))
|
||||||
self.result = "run"
|
self.result = "run"
|
||||||
self.afterQueue = "finish"
|
self.afterQueue = "finish"
|
||||||
return
|
return
|
||||||
@@ -4006,12 +4065,12 @@ function BattleState:safariAction(choice)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if choice == "bait" then
|
if choice == "bait" then
|
||||||
self:say(Strings("%s threw some\nBAIT.", playerName))
|
self:say(self:romText("_ThrewBaitText", "%s threw some\nBAIT.", playerName))
|
||||||
self.safariCatchRate = math.floor(self.safariCatchRate / 2)
|
self.safariCatchRate = math.floor(self.safariCatchRate / 2)
|
||||||
self.baitFactor = math.min(255, self.baitFactor + self.rng(1, 5))
|
self.baitFactor = math.min(255, self.baitFactor + self.rng(1, 5))
|
||||||
self.escapeFactor = 0
|
self.escapeFactor = 0
|
||||||
else -- rock
|
else -- rock
|
||||||
self:say(Strings("%s threw a\nROCK.", playerName))
|
self:say(self:romText("_ThrewRockText", "%s threw a\nROCK.", playerName))
|
||||||
self.safariCatchRate = math.min(255, self.safariCatchRate * 2)
|
self.safariCatchRate = math.min(255, self.safariCatchRate * 2)
|
||||||
self.escapeFactor = math.min(255, self.escapeFactor + self.rng(1, 5))
|
self.escapeFactor = math.min(255, self.escapeFactor + self.rng(1, 5))
|
||||||
self.baitFactor = 0
|
self.baitFactor = 0
|
||||||
@@ -4027,13 +4086,13 @@ end
|
|||||||
function BattleState:safariEnemyTurn()
|
function BattleState:safariEnemyTurn()
|
||||||
if self.baitFactor > 0 then
|
if self.baitFactor > 0 then
|
||||||
self.baitFactor = self.baitFactor - 1
|
self.baitFactor = self.baitFactor - 1
|
||||||
self:sayNext(Strings("Wild %s\nis eating!", self.enemy.name))
|
self:sayNext(self:romText("_SafariZoneEatingText", "Wild %s\nis eating!", self.enemy.name))
|
||||||
elseif self.escapeFactor > 0 then
|
elseif self.escapeFactor > 0 then
|
||||||
self.escapeFactor = self.escapeFactor - 1
|
self.escapeFactor = self.escapeFactor - 1
|
||||||
if self.escapeFactor == 0 then
|
if self.escapeFactor == 0 then
|
||||||
self.safariCatchRate = self.enemy.def.catchRate
|
self.safariCatchRate = self.enemy.def.catchRate
|
||||||
end
|
end
|
||||||
self:sayNext(Strings("Wild %s\nis angry!", self.enemy.name))
|
self:sayNext(self:romText("_SafariZoneAngryText", "Wild %s\nis angry!", self.enemy.name))
|
||||||
end
|
end
|
||||||
self:act(function()
|
self:act(function()
|
||||||
local speed = self.enemy.curStats.speed % 256
|
local speed = self.enemy.curStats.speed % 256
|
||||||
@@ -4049,7 +4108,7 @@ function BattleState:safariEnemyTurn()
|
|||||||
fled = self.rng(0, 255) < b
|
fled = self.rng(0, 255) < b
|
||||||
end
|
end
|
||||||
if fled then
|
if fled then
|
||||||
self:sayNext(Strings("Wild %s\nran!", self.enemy.name))
|
self:sayNext(self:romText("_WildRanText", "Wild %s\nran!", self.enemy.name))
|
||||||
self:actNext(function()
|
self:actNext(function()
|
||||||
require("src.core.Sound").play(self.data, "Run")
|
require("src.core.Sound").play(self.data, "Run")
|
||||||
startPicKind(self:picFxFor(self.enemy), "slideOff")
|
startPicKind(self:picFxFor(self.enemy), "slideOff")
|
||||||
@@ -4116,11 +4175,11 @@ function BattleState:tryRun()
|
|||||||
TurnOrder.effectiveSpeed(self.enemy))
|
TurnOrder.effectiveSpeed(self.enemy))
|
||||||
if escaped then
|
if escaped then
|
||||||
require("src.core.Sound").play(self.data, "Run")
|
require("src.core.Sound").play(self.data, "Run")
|
||||||
self:say(Strings("Got away safely!"))
|
self:say(self:romText("_GotAwayText", "Got away safely!"))
|
||||||
self.result = "run"
|
self.result = "run"
|
||||||
self.afterQueue = "finish"
|
self.afterQueue = "finish"
|
||||||
else
|
else
|
||||||
self:say(Strings("Can't escape!"))
|
self:say(self:romText("_CantEscapeText", "Can't escape!"))
|
||||||
self:act(function()
|
self:act(function()
|
||||||
self:executeAction(self.enemy, self.player, self:enemyAction())
|
self:executeAction(self.enemy, self.player, self:enemyAction())
|
||||||
end)
|
end)
|
||||||
@@ -4157,9 +4216,9 @@ function BattleState:ballMissMessage(shakes)
|
|||||||
elseif shakes == 1 then
|
elseif shakes == 1 then
|
||||||
return t._ItemUseBallText02 or Strings("Darn! The POKéMON\nbroke free!")
|
return t._ItemUseBallText02 or Strings("Darn! The POKéMON\nbroke free!")
|
||||||
elseif shakes == 2 then
|
elseif shakes == 2 then
|
||||||
return (t._ItemUseBallText03 or Strings("Aww! It appeared\nto be caught!")):gsub("%s+$", "")
|
return (t._ItemUseBallText03 or self:romText("_ItemUseBallText03", "Aww! It appeared\nto be caught!")):gsub("%s+$", "")
|
||||||
end
|
end
|
||||||
return t._ItemUseBallText04 or Strings("Shoot! It was so\nclose too!")
|
return t._ItemUseBallText04 or self:romText("_ItemUseBallText04", "Shoot! It was so\nclose too!")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- AskName (engine/menus/naming_screen.asm): ClearSprites, wild field blank,
|
-- AskName (engine/menus/naming_screen.asm): ClearSprites, wild field blank,
|
||||||
@@ -4170,7 +4229,7 @@ function BattleState:askNicknameUI(mon, displayName)
|
|||||||
self.lockedBall = nil
|
self.lockedBall = nil
|
||||||
self.blankForAskName = true
|
self.blankForAskName = true
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
local text = Strings("Do you want to\ngive a nickname\nto %s?", displayName)
|
local text = self:romText("_DoYouWantToNicknameText", "Do you want to\ngive a nickname\nto %s?", displayName)
|
||||||
local label = game.data.text and game.data.text._DoYouWantToNicknameText
|
local label = game.data.text and game.data.text._DoYouWantToNicknameText
|
||||||
if label then
|
if label then
|
||||||
-- extractor CONT is \t; TextBox scrolls on \n/\v
|
-- extractor CONT is \t; TextBox scrolls on \n/\v
|
||||||
@@ -4299,7 +4358,7 @@ function BattleState:throwBall(ball)
|
|||||||
-- "<PLAYER> used <ITEM>!" line (#291). Safari and the old man demo are
|
-- "<PLAYER> used <ITEM>!" line (#291). Safari and the old man demo are
|
||||||
-- still wIsInBattle == 1, and this port models both as kind == "wild".
|
-- still wIsInBattle == 1, and this port models both as kind == "wild".
|
||||||
if self.kind == "wild" then
|
if self.kind == "wild" then
|
||||||
self:say(Strings("%s used\n%s!", self.game.save.player.name,
|
self:say(self:romText("_ItemUseText001", "%s used\n%s!", self.game.save.player.name,
|
||||||
self.data.items[ball].name))
|
self.data.items[ball].name))
|
||||||
end
|
end
|
||||||
self:act(function()
|
self:act(function()
|
||||||
@@ -4321,9 +4380,9 @@ function BattleState:throwBall(ball)
|
|||||||
end)
|
end)
|
||||||
self:animNext("BLOCKBALL_ANIM", true)
|
self:animNext("BLOCKBALL_ANIM", true)
|
||||||
self:sayNext(t._ThrowBallAtTrainerMonText1
|
self:sayNext(t._ThrowBallAtTrainerMonText1
|
||||||
or Strings("The trainer\nblocked the BALL!"))
|
or self:romText("_ThrowBallAtTrainerMonText1", "The trainer\nblocked the BALL!"))
|
||||||
self:sayNext(t._ThrowBallAtTrainerMonText2
|
self:sayNext(t._ThrowBallAtTrainerMonText2
|
||||||
or Strings("Don't be a thief!"))
|
or self:romText("_ThrowBallAtTrainerMonText2", "Don't be a thief!"))
|
||||||
self:act(function()
|
self:act(function()
|
||||||
self:executeAction(self.enemy, self.player, self:enemyAction())
|
self:executeAction(self.enemy, self.player, self:enemyAction())
|
||||||
end)
|
end)
|
||||||
@@ -4386,7 +4445,7 @@ function BattleState:openParty()
|
|||||||
if mon == self.player.mon then
|
if mon == self.player.mon then
|
||||||
self:say(Strings("%s is\nalready out!", self.player.name))
|
self:say(Strings("%s is\nalready out!", self.player.name))
|
||||||
elseif mon.hp <= 0 then
|
elseif mon.hp <= 0 then
|
||||||
self:say(Strings("There's no will\nto fight!"))
|
self:say(self:romText("_NoWillText", "There's no will\nto fight!"))
|
||||||
else
|
else
|
||||||
self:resolveSwitch(mon)
|
self:resolveSwitch(mon)
|
||||||
end
|
end
|
||||||
@@ -4412,7 +4471,7 @@ end
|
|||||||
function BattleState:finish()
|
function BattleState:finish()
|
||||||
if self.payDay and self.result == "win" then
|
if self.payDay and self.result == "win" then
|
||||||
self.game.save.money = self.game.save.money + self.payDay
|
self.game.save.money = self.game.save.money + self.payDay
|
||||||
self:say(Strings("%s picked up\n¥%d!", self.game.save.player.name, self.payDay))
|
self:say(self:romText("_PickUpPayDayMoneyText", "%s picked up\n¥%d!", self.game.save.player.name, self.payDay))
|
||||||
self.payDay = nil
|
self.payDay = nil
|
||||||
self.afterQueue = "finish"
|
self.afterQueue = "finish"
|
||||||
self.phase = "messages"
|
self.phase = "messages"
|
||||||
@@ -4542,11 +4601,15 @@ end
|
|||||||
-- pixels, so it scales with the pic's draw scale (the player's default 2x
|
-- pixels, so it scales with the pic's draw scale (the player's default 2x
|
||||||
-- sinks 2x as fast to sink at the same visual rate); a mod scale composes
|
-- sinks 2x as fast to sink at the same visual rate); a mod scale composes
|
||||||
-- the same way. scale defaults to the vanilla side scale when unknown.
|
-- the same way. scale defaults to the vanilla side scale when unknown.
|
||||||
|
-- SlideDownFaintedMonPic drops the pic one 8px row per 2-frame step, so
|
||||||
|
-- the offset advances Timing.FAINT_SLIDE_STEP (4px) per frame at 1x --
|
||||||
|
-- the full 56px PIC_HEIGHT slide over the 14-frame budget (#671: the
|
||||||
|
-- old (30 - frames) math teleported the sprite 32px down on frame one).
|
||||||
function BattleState:fxFaintOffset(battler, scale)
|
function BattleState:fxFaintOffset(battler, scale)
|
||||||
local fx = self.fx
|
local fx = self.fx
|
||||||
if self:fxFaintActive(battler) then
|
if self:fxFaintActive(battler) then
|
||||||
scale = scale or (battler.isPlayer and 2 or 1)
|
scale = scale or (battler.isPlayer and 2 or 1)
|
||||||
return (30 - fx.faint.frames) * 2 * scale
|
return (Timing.FAINT_SLIDE - fx.faint.frames) * Timing.FAINT_SLIDE_STEP * scale
|
||||||
end
|
end
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -315,6 +315,15 @@ end
|
|||||||
-- everything else here: the text box and YES/NO a battle puts up are states
|
-- everything else here: the text box and YES/NO a battle puts up are states
|
||||||
-- of their own sitting above it, and they are exactly the elements that must
|
-- of their own sitting above it, and they are exactly the elements that must
|
||||||
-- stay inside the battle's composition rather than dock to the window.
|
-- stay inside the battle's composition rather than dock to the window.
|
||||||
|
-- UI LAYOUT: is edge docking switched on? Only the explicit "dynamic" turns
|
||||||
|
-- it on, so a save written before the option existed -- and any caller with no
|
||||||
|
-- save at all, which is most of the headless suites -- gets CENTERED, the
|
||||||
|
-- behaviour the port shipped with.
|
||||||
|
function Game.dynamicUI(save)
|
||||||
|
local options = save and save.options
|
||||||
|
return options ~= nil and options.uiLayout == "dynamic"
|
||||||
|
end
|
||||||
|
|
||||||
function Game.uiAnchorsHeldInStack(stack)
|
function Game.uiAnchorsHeldInStack(stack)
|
||||||
for i = #(stack and stack.states or {}), 1, -1 do
|
for i = #(stack and stack.states or {}), 1, -1 do
|
||||||
local state = stack.states[i]
|
local state = stack.states[i]
|
||||||
@@ -415,6 +424,13 @@ function Game:draw()
|
|||||||
Renderer.uiWorldHold = Renderer.battleDim ~= nil
|
Renderer.uiWorldHold = Renderer.battleDim ~= nil
|
||||||
-- ...and a battle keeps its dialogue box and YES/NO inside its own screen
|
-- ...and a battle keeps its dialogue box and YES/NO inside its own screen
|
||||||
-- instead of letting them dock to the window edge.
|
-- instead of letting them dock to the window edge.
|
||||||
|
-- UI LAYOUT: CENTERED (the default) is a fixed letterbox -- every element
|
||||||
|
-- stays inside the 160x144 canvas and the UI does not follow the survey
|
||||||
|
-- zoom, so the screen furniture never moves or resizes under the player.
|
||||||
|
-- That is the composition the port shipped with. DYNAMIC opts into both
|
||||||
|
-- halves: the dialogue box docks to the window's bottom edge, the START
|
||||||
|
-- menu to its top right, and the whole UI steps down with the zoom.
|
||||||
|
Renderer.uiCentered = not Game.dynamicUI(self.save)
|
||||||
Renderer.uiAnchorHold = Game.uiAnchorsHeldInStack(self.stack)
|
Renderer.uiAnchorHold = Game.uiAnchorsHeldInStack(self.stack)
|
||||||
Renderer:beginFrame(worldBelow)
|
Renderer:beginFrame(worldBelow)
|
||||||
for i = drawFrom, #self.stack.states do
|
for i = drawFrom, #self.stack.states do
|
||||||
|
|||||||
@@ -228,6 +228,15 @@ function SaveData.defaultOptions()
|
|||||||
-- "black" = plain black bars, "world" = the frozen overworld showing
|
-- "black" = plain black bars, "world" = the frozen overworld showing
|
||||||
-- through, dimmed. See BattleState:bgMode.
|
-- through, dimmed. See BattleState:bgMode.
|
||||||
battleBg = "white",
|
battleBg = "white",
|
||||||
|
-- UI LAYOUT: "centered" = a fixed letterbox. Every element sits where it
|
||||||
|
-- was drawn in the 160x144 canvas and the UI does not follow the survey
|
||||||
|
-- zoom, so nothing moves or resizes under the player. The original
|
||||||
|
-- composition. "dynamic" = the dialogue box docks to the window's bottom
|
||||||
|
-- edge, the START menu to its top right, and the UI steps down with the
|
||||||
|
-- zoom. Centered is the default: dynamic reads better zoomed out, but it
|
||||||
|
-- moves the screen furniture, so it is opt-in.
|
||||||
|
-- See Game.dynamicUI, Renderer:setUIAnchor and Renderer:uiScale.
|
||||||
|
uiLayout = "centered",
|
||||||
ruleset = "gen1_faithful",
|
ruleset = "gen1_faithful",
|
||||||
-- 0-7 like the GB's NR50 master volume
|
-- 0-7 like the GB's NR50 master volume
|
||||||
musicVol = 7,
|
musicVol = 7,
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ Timing.NO_MOVES_LEFT = 60 -- core.asm:2753-2754
|
|||||||
Timing.TRAINER_VICTORY = 40 -- core.asm:940-941
|
Timing.TRAINER_VICTORY = 40 -- core.asm:940-941
|
||||||
Timing.PLAYER_BLACKOUT = 40 -- core.asm:1143-1144
|
Timing.PLAYER_BLACKOUT = 40 -- core.asm:1143-1144
|
||||||
Timing.FAINT_SLIDE_ROW = 2 -- core.asm:1216-1217, per row
|
Timing.FAINT_SLIDE_ROW = 2 -- core.asm:1216-1217, per row
|
||||||
|
Timing.FAINT_SLIDE_STEP = 8 / Timing.FAINT_SLIDE_ROW -- 4px per frame at 1x
|
||||||
Timing.TRAINER_SLIDE_COL = 2 -- core.asm:1267-1268, per column
|
Timing.TRAINER_SLIDE_COL = 2 -- core.asm:1267-1268, per column
|
||||||
|
|
||||||
-- HP bar (engine/gfx/hp_bar.asm) ---------------------------------------------
|
-- HP bar (engine/gfx/hp_bar.asm) ---------------------------------------------
|
||||||
|
|||||||
@@ -142,6 +142,13 @@ end
|
|||||||
function Renderer:uiScale()
|
function Renderer:uiScale()
|
||||||
local S = self:fitScale()
|
local S = self:fitScale()
|
||||||
local off = Zoom.offset or 0
|
local off = Zoom.offset or 0
|
||||||
|
-- UI LAYOUT = CENTERED (uiCentered, set per frame by Game:draw): the UI is
|
||||||
|
-- a fixed letterbox at the fit scale and does not follow the survey zoom at
|
||||||
|
-- all, which is the whole point of the setting -- the screen furniture
|
||||||
|
-- stays put instead of resizing under the player. DYNAMIC keeps the
|
||||||
|
-- step-down below. BATTLE SIZE is unaffected either way: uiFill overrides
|
||||||
|
-- the scale in endFrame, after this.
|
||||||
|
if self.uiCentered then return S end
|
||||||
-- Only follow the zoom when a world is actually behind the UI. Survey zoom
|
-- Only follow the zoom when a world is actually behind the UI. Survey zoom
|
||||||
-- is an OVERWORLD control; the title screen, the intro and the credits show
|
-- is an OVERWORLD control; the title screen, the intro and the credits show
|
||||||
-- no world at all, and shrinking them to match a zoom level the player set
|
-- no world at all, and shrinking them to match a zoom level the player set
|
||||||
@@ -630,6 +637,14 @@ end
|
|||||||
-- pixels, and consumed by endFrame this frame only.
|
-- pixels, and consumed by endFrame this frame only.
|
||||||
-- anchor: "bottom" | "topright" | "topleft" | "bottomright"
|
-- anchor: "bottom" | "topright" | "topleft" | "bottomright"
|
||||||
function Renderer:setUIAnchor(x, y, w, h, anchor)
|
function Renderer:setUIAnchor(x, y, w, h, anchor)
|
||||||
|
-- UI LAYOUT = CENTERED (uiCentered, set per frame by Game:draw from
|
||||||
|
-- save.options.uiLayout): every element stays where it was drawn in the
|
||||||
|
-- 160x144 canvas and the letterbox centres the lot, which is how the port
|
||||||
|
-- behaved before edge docking existed. This is the DEFAULT; DYNAMIC opts
|
||||||
|
-- back into docking. Gating here rather than at each caller means one
|
||||||
|
-- switch covers the dialogue box, its YES/NO, the START menu and anything
|
||||||
|
-- added later, and none of them has to know the option exists.
|
||||||
|
if self.uiCentered then return end
|
||||||
-- uiAnchorHold (Game:draw): a state that composes its own screen -- a
|
-- uiAnchorHold (Game:draw): a state that composes its own screen -- a
|
||||||
-- battle -- keeps every element inside it, so the box blits where it was
|
-- battle -- keeps every element inside it, so the box blits where it was
|
||||||
-- drawn in the canvas instead of being pulled to the window edge.
|
-- drawn in the canvas instead of being pulled to the window edge.
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ local OakSpeech = {}
|
|||||||
OakSpeech.__index = OakSpeech
|
OakSpeech.__index = OakSpeech
|
||||||
OakSpeech.isOpaque = true
|
OakSpeech.isOpaque = true
|
||||||
|
|
||||||
|
-- The speech is a white field with a pic on it, and its dialogue box docks to
|
||||||
|
-- the WINDOW's bottom edge (Renderer:setUIAnchor, via TextBox). The white it
|
||||||
|
-- fills below is only the 160x144 UI canvas, so once the box moved to the
|
||||||
|
-- window edge the two stopped touching: black letterbox showed between the
|
||||||
|
-- bottom of Oak's white and the top of the box he is speaking from. Filling
|
||||||
|
-- the voids with the paper shade -- the same opt-in a battle uses -- puts the
|
||||||
|
-- box back on the field. Not a literal 1,1,1: the canvas is colorized, so
|
||||||
|
-- endFrame matches it with PaletteFX.paperShade.
|
||||||
|
OakSpeech.letterboxWhite = true
|
||||||
|
|
||||||
-- FadeInIntroPic runs a 6-step palette fade; MovePicLeft wipes the mon
|
-- FadeInIntroPic runs a 6-step palette fade; MovePicLeft wipes the mon
|
||||||
-- sprite in from the right. Both play out before the beat's text prints.
|
-- sprite in from the right. Both play out before the beat's text prints.
|
||||||
local FADE_FRAMES = 24
|
local FADE_FRAMES = 24
|
||||||
@@ -46,6 +56,8 @@ local FALLBACKS = {
|
|||||||
_OakSpeechText3 = Strings.source("{PLAYER}!\fYour very own\nPOKéMON legend is\vabout to unfold!\fA world of dreams\nand adventures\vwith POKéMON\vawaits! Let's go!"),
|
_OakSpeechText3 = Strings.source("{PLAYER}!\fYour very own\nPOKéMON legend is\vabout to unfold!\fA world of dreams\nand adventures\vwith POKéMON\vawaits! Let's go!"),
|
||||||
_IntroducePlayerText = Strings.source("First, what is\nyour name?"),
|
_IntroducePlayerText = Strings.source("First, what is\nyour name?"),
|
||||||
_IntroduceRivalText = Strings.source("This is my grand-\nson. He's been\vyour rival since\vyou were a baby.\f...Erm, what is\nhis name again?"),
|
_IntroduceRivalText = Strings.source("This is my grand-\nson. He's been\vyour rival since\vyou were a baby.\f...Erm, what is\nhis name again?"),
|
||||||
|
_YourNameIsText = Strings.source("Right! So your\nname is {PLAYER}!"),
|
||||||
|
_HisNameIsText = Strings.source("That's right! I\nremember now! His\vname is {RIVAL}!"),
|
||||||
}
|
}
|
||||||
|
|
||||||
local function textOr(game, key)
|
local function textOr(game, key)
|
||||||
@@ -152,6 +164,14 @@ function OakSpeech.defaultSteps(speech)
|
|||||||
presetsWho = "player",
|
presetsWho = "player",
|
||||||
presetsFallback = { "RED", "ASH", "JACK" },
|
presetsFallback = { "RED", "ASH", "JACK" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
-- oak_speech.asm prints YourNameIsText right after the naming screen
|
||||||
|
-- returns ("Right! So your name is RED!"); the port went straight on
|
||||||
|
-- to the rival and dropped it, in every language.
|
||||||
|
id = "confirm_player_name",
|
||||||
|
kind = "say",
|
||||||
|
textKey = "_YourNameIsText",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id = "ask_rival_name",
|
id = "ask_rival_name",
|
||||||
kind = "say",
|
kind = "say",
|
||||||
@@ -166,6 +186,12 @@ function OakSpeech.defaultSteps(speech)
|
|||||||
presetsWho = "rival",
|
presetsWho = "rival",
|
||||||
presetsFallback = { "BLUE", "GARY", "JOHN" },
|
presetsFallback = { "BLUE", "GARY", "JOHN" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
-- HisNameIsText, the rival's counterpart to the confirmation above
|
||||||
|
id = "confirm_rival_name",
|
||||||
|
kind = "say",
|
||||||
|
textKey = "_HisNameIsText",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id = "legend",
|
id = "legend",
|
||||||
kind = "say",
|
kind = "say",
|
||||||
@@ -611,6 +637,15 @@ function OakSpeech:draw()
|
|||||||
love.graphics.draw(self.walkSheet, self.walkQuad, 64, 60)
|
love.graphics.draw(self.walkSheet, self.walkQuad, 64, 60)
|
||||||
end
|
end
|
||||||
if self.shrinkText then
|
if self.shrinkText then
|
||||||
|
-- This is a REPLICA of the dialogue box that just closed, redrawn at
|
||||||
|
-- TextBox's own rect (BOX_TX..BOX_TH = 0,12,20,6) so the last page holds
|
||||||
|
-- while the pic shrinks. The real box rides the bottom anchor, so this
|
||||||
|
-- one has to as well -- otherwise the text visibly jumps up a letterbox
|
||||||
|
-- on the frame the real box is swapped for this copy.
|
||||||
|
local r = self.game and self.game.renderer
|
||||||
|
if r and r.setUIAnchor then
|
||||||
|
r:setUIAnchor(0, 12 * 8, 20 * 8, 6 * 8, "bottom")
|
||||||
|
end
|
||||||
Font.drawBox(0, 12, 20, 6)
|
Font.drawBox(0, 12, 20, 6)
|
||||||
love.graphics.setColor(0, 0, 0, 1)
|
love.graphics.setColor(0, 0, 0, 1)
|
||||||
for i, line in ipairs(self.shrinkText) do
|
for i, line in ipairs(self.shrinkText) do
|
||||||
|
|||||||
@@ -191,6 +191,23 @@ local function buildRows(game)
|
|||||||
o.battleBg = order[(cur - 1 + (dir or 1)) % #order + 1]
|
o.battleBg = order[(cur - 1 + (dir or 1)) % #order + 1]
|
||||||
return true
|
return true
|
||||||
end },
|
end },
|
||||||
|
-- CENTERED is a fixed letterbox: elements stay inside the 160x144 canvas
|
||||||
|
-- and the UI does not follow the survey zoom, so nothing moves or resizes
|
||||||
|
-- under the player. The composition the port shipped with. DYNAMIC docks
|
||||||
|
-- the dialogue box to the window's bottom edge and the START menu to its
|
||||||
|
-- top right, and steps the UI down with the zoom -- easier to read zoomed
|
||||||
|
-- out, but it moves furniture the original never moved, so it is opt-in.
|
||||||
|
-- BATTLE SIZE is independent of this and works under either.
|
||||||
|
{ id = "uiLayout", label = Strings("UI LAYOUT"),
|
||||||
|
value = function(g)
|
||||||
|
return g.save.options.uiLayout == "dynamic" and Strings("DYNAMIC")
|
||||||
|
or Strings("CENTERED")
|
||||||
|
end,
|
||||||
|
step = function(g)
|
||||||
|
local o = g.save.options
|
||||||
|
o.uiLayout = o.uiLayout == "dynamic" and "centered" or "dynamic"
|
||||||
|
return true
|
||||||
|
end },
|
||||||
{ id = "ruleset", label = Strings("RULESET"),
|
{ id = "ruleset", label = Strings("RULESET"),
|
||||||
value = function(g) return rulesetName(g) end,
|
value = function(g) return rulesetName(g) end,
|
||||||
step = function(g, dir)
|
step = function(g, dir)
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
-- Driver: Oak's intro white field must reach the dialogue box.
|
||||||
|
--
|
||||||
|
-- The speech fills white over the 160x144 UI canvas, but TextBox docks to the
|
||||||
|
-- WINDOW's bottom edge (Renderer:setUIAnchor). In a letterboxed window that
|
||||||
|
-- left black between the bottom of the white and the top of the box.
|
||||||
|
-- OakSpeech.letterboxWhite fills the voids with the paper shade instead.
|
||||||
|
--
|
||||||
|
-- Needs a window that actually letterboxes -- an exact multiple of 160x144
|
||||||
|
-- has no voids to get wrong -- so it resizes before shooting.
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/oak_speech_letterbox_test.lua lovec .
|
||||||
|
return function(game)
|
||||||
|
local U = dofile("tests/drivers/util.lua")
|
||||||
|
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||||
|
local OakSpeech = require("src.ui.OakSpeech")
|
||||||
|
|
||||||
|
local function speechUp()
|
||||||
|
for _, s in ipairs(game.stack.states or {}) do
|
||||||
|
if getmetatable(s) == OakSpeech then return s end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 1000x700 is not a multiple of 160x144, so the UI blits at 4x (640x576)
|
||||||
|
-- with real bars above/below -- exactly where the seam shows
|
||||||
|
love.window.setMode(1000, 700)
|
||||||
|
U.wait(30)
|
||||||
|
|
||||||
|
U.wait(5)
|
||||||
|
U.tap(game, "start") -- skip the intro movie
|
||||||
|
U.wait(20)
|
||||||
|
U.tap(game, "a") -- title -> menu
|
||||||
|
U.wait(20)
|
||||||
|
U.tap(game, "a") -- NEW GAME
|
||||||
|
U.wait(30)
|
||||||
|
|
||||||
|
local speech
|
||||||
|
for _ = 1, 600 do
|
||||||
|
speech = speechUp()
|
||||||
|
if speech then break end
|
||||||
|
U.wait(2)
|
||||||
|
end
|
||||||
|
if not speech then
|
||||||
|
U.log("FAIL never reached Oak's speech")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
U.log("Oak speech is up; letterboxWhite =", tostring(OakSpeech.letterboxWhite))
|
||||||
|
|
||||||
|
-- page through, shooting a few beats: the pic + box together is the shot
|
||||||
|
-- that shows whether the white reaches the box
|
||||||
|
for i = 1, 4 do
|
||||||
|
for _ = 1, 200 do
|
||||||
|
local top = game.stack:top()
|
||||||
|
if top and top ~= speech and top.done then break end
|
||||||
|
U.tap(game, "a")
|
||||||
|
U.wait(2)
|
||||||
|
if not speechUp() then break end
|
||||||
|
end
|
||||||
|
if not speechUp() then break end
|
||||||
|
U.wait(20)
|
||||||
|
U.shot(game, DIR .. ("/oak_%d.png"):format(i))
|
||||||
|
U.tap(game, "a")
|
||||||
|
U.wait(20)
|
||||||
|
end
|
||||||
|
|
||||||
|
U.log("done")
|
||||||
|
U.wait(30)
|
||||||
|
end
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
-- Driver: UI LAYOUT centered vs dynamic, same moment shot twice.
|
||||||
|
--
|
||||||
|
-- Only visible when the window letterboxes, so it resizes first: at an exact
|
||||||
|
-- multiple of 160x144 there is nowhere for a docked element to dock TO.
|
||||||
|
-- Shoots the overworld dialogue box and the START menu, the two pieces the
|
||||||
|
-- option moves.
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/ui_layout_option_test.lua lovec .
|
||||||
|
return function(game)
|
||||||
|
local U = dofile("tests/drivers/util.lua")
|
||||||
|
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||||
|
|
||||||
|
love.window.setMode(1000, 700)
|
||||||
|
U.wait(30)
|
||||||
|
|
||||||
|
-- straight into the overworld, no intro
|
||||||
|
U.teleport(game, "PALLET_TOWN", 5, 6, "down")
|
||||||
|
U.wait(40)
|
||||||
|
|
||||||
|
local function shootBoth(tag)
|
||||||
|
-- START menu (Menu anchors "topright")
|
||||||
|
U.tap(game, "start")
|
||||||
|
U.wait(45)
|
||||||
|
U.shot(game, DIR .. ("/uilayout_%s_startmenu.png"):format(tag))
|
||||||
|
U.tap(game, "b")
|
||||||
|
U.wait(30)
|
||||||
|
|
||||||
|
-- a dialogue box (TextBox anchors "bottom"): read the sign by the door
|
||||||
|
local TextBox = require("src.render.TextBox")
|
||||||
|
game.stack:push(TextBox.new(game, "UI LAYOUT check:\nthis box.", function() end))
|
||||||
|
for _ = 1, 400 do
|
||||||
|
local top = game.stack:top()
|
||||||
|
if top and top.done then break end
|
||||||
|
U.wait(2)
|
||||||
|
end
|
||||||
|
U.wait(30)
|
||||||
|
U.shot(game, DIR .. ("/uilayout_%s_textbox.png"):format(tag))
|
||||||
|
game.stack:pop()
|
||||||
|
U.wait(20)
|
||||||
|
end
|
||||||
|
|
||||||
|
game.save.options.uiLayout = "centered"
|
||||||
|
U.log("UI LAYOUT = centered (the default)")
|
||||||
|
U.wait(20)
|
||||||
|
shootBoth("centered")
|
||||||
|
|
||||||
|
game.save.options.uiLayout = "dynamic"
|
||||||
|
U.log("UI LAYOUT = dynamic")
|
||||||
|
U.wait(20)
|
||||||
|
shootBoth("dynamic")
|
||||||
|
|
||||||
|
U.log("done")
|
||||||
|
U.wait(20)
|
||||||
|
end
|
||||||
@@ -62,10 +62,19 @@ end
|
|||||||
-- Blank out every Strings(...) / Strings.source(...) call span, parens
|
-- Blank out every Strings(...) / Strings.source(...) call span, parens
|
||||||
-- balanced, so a call wrapped across lines counts as covered. A per-line
|
-- balanced, so a call wrapped across lines counts as covered. A per-line
|
||||||
-- test reported the continuation lines of three real calls as misses.
|
-- test reported the continuation lines of three real calls as misses.
|
||||||
|
--
|
||||||
|
-- romText(...) counts as a router too: it prefers the line the importer
|
||||||
|
-- extracted from the ROM and hands its literal straight to Strings(...)
|
||||||
|
-- whenever that label is absent (a cache built before it, or a dataset-less
|
||||||
|
-- unit test), so the literal is still catalog-backed and a translation mod
|
||||||
|
-- still reaches it. Blanking the whole span is safe -- the only literals
|
||||||
|
-- inside are the pokered label and that fallback.
|
||||||
local function stripStringsCalls(body)
|
local function stripStringsCalls(body)
|
||||||
local out, i, n = {}, 1, #body
|
local out, i, n = {}, 1, #body
|
||||||
while i <= n do
|
while i <= n do
|
||||||
local s, e = body:find("Strings%.?s?o?u?r?c?e?%(", i)
|
local s, e = body:find("Strings%.?s?o?u?r?c?e?%(", i)
|
||||||
|
local rs = body:find("romText%(", i)
|
||||||
|
if rs and (not s or rs < s) then s, e = rs, nil end
|
||||||
if not s then out[#out + 1] = body:sub(i) break end
|
if not s then out[#out + 1] = body:sub(i) break end
|
||||||
out[#out + 1] = body:sub(i, s - 1)
|
out[#out + 1] = body:sub(i, s - 1)
|
||||||
local depth, j = 0, body:find("%(", s)
|
local depth, j = 0, body:find("%(", s)
|
||||||
|
|||||||
@@ -94,11 +94,18 @@ T.check(box:find("Those are", 1, true) == nil,
|
|||||||
T.check(box:find("#MON", 1, true) == nil,
|
T.check(box:find("#MON", 1, true) == nil,
|
||||||
"the ROM #MON ligature is spelled out as Pokémon")
|
"the ROM #MON ligature is spelled out as Pokémon")
|
||||||
|
|
||||||
-- the pokered beat also turns Oak to face the player
|
-- the pokered beat also turns Oak to face the player: find the
|
||||||
T.check(contribution.talk[BALL][20][1] == "face_object"
|
-- face_object row in the leftover-ball path (content-based, so a jingle
|
||||||
and contribution.talk[BALL][20][2] == 5
|
-- row added for #668 doesn't shift the hard-coded index)
|
||||||
and contribution.talk[BALL][20][3] == "down",
|
local oakFaceRow
|
||||||
"row 20 faces Oak down before the line (OaksLabLastMonScript)")
|
for i, row in ipairs(contribution.talk[BALL]) do
|
||||||
|
if row[1] == "face_object" and row[2] == 5 and row[3] == "down" then
|
||||||
|
oakFaceRow = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
T.check(oakFaceRow ~= nil,
|
||||||
|
"a face_object row turns Oak down before the line (OaksLabLastMonScript)")
|
||||||
|
|
||||||
-- ---- pre-escort: still the vanilla "Those are POKé BALLs" line
|
-- ---- pre-escort: still the vanilla "Those are POKé BALLs" line
|
||||||
local pre = { EVENT_GOT_STARTER = false, EVENT_FOLLOWED_OAK_INTO_LAB = false }
|
local pre = { EVENT_GOT_STARTER = false, EVENT_FOLLOWED_OAK_INTO_LAB = false }
|
||||||
@@ -115,15 +122,25 @@ T.check(concat(t3):find("last Pokémon!", 1, true) == nil,
|
|||||||
"no last-mon line before the pick")
|
"no last-mon line before the pick")
|
||||||
|
|
||||||
-- ---- all three balls share the same table shape (last-mon beat present)
|
-- ---- all three balls share the same table shape (last-mon beat present)
|
||||||
|
-- content-based again: locate the leftover-ball "Pokémon" line wherever
|
||||||
|
-- it sits, instead of pinning a row number (#668 added two jingle rows)
|
||||||
for _, key in ipairs({
|
for _, key in ipairs({
|
||||||
"TEXT_OAKSLAB_CHARMANDER_POKE_BALL",
|
"TEXT_OAKSLAB_CHARMANDER_POKE_BALL",
|
||||||
"TEXT_OAKSLAB_SQUIRTLE_POKE_BALL",
|
"TEXT_OAKSLAB_SQUIRTLE_POKE_BALL",
|
||||||
"TEXT_OAKSLAB_BULBASAUR_POKE_BALL",
|
"TEXT_OAKSLAB_BULBASAUR_POKE_BALL",
|
||||||
}) do
|
}) do
|
||||||
local script = contribution.talk[key]
|
local script = contribution.talk[key]
|
||||||
T.check(script and script[21] and script[21][2] and
|
local lastMon
|
||||||
script[21][2]:find("Pokémon", 1, true) ~= nil,
|
if script then
|
||||||
key .. " carries the last-mon line")
|
for _, row in ipairs(script) do
|
||||||
|
if row[1] == "show_text" and type(row[2]) == "string"
|
||||||
|
and row[2]:find("Pokémon", 1, true) then
|
||||||
|
lastMon = row[2]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
T.check(lastMon ~= nil, key .. " carries the last-mon line")
|
||||||
end
|
end
|
||||||
|
|
||||||
T.finish("oaks_lab_last_ball_bug601")
|
T.finish("oaks_lab_last_ball_bug601")
|
||||||
|
|||||||
@@ -273,6 +273,26 @@ T.eq(Timing.BLINK_MON % 10, 0,
|
|||||||
-- SlideDownFaintedMonPic: b = PIC_HEIGHT slide steps of DelayFrames 2
|
-- SlideDownFaintedMonPic: b = PIC_HEIGHT slide steps of DelayFrames 2
|
||||||
T.eq(Timing.FAINT_SLIDE, 14, "the faint slide is 7 steps x 2 frames")
|
T.eq(Timing.FAINT_SLIDE, 14, "the faint slide is 7 steps x 2 frames")
|
||||||
|
|
||||||
|
-- #671: the slide must start at the sprite's resting spot and sink the
|
||||||
|
-- full PIC_HEIGHT (7 rows x 8px = 56px at 1x) across those 14 frames.
|
||||||
|
-- The old (30 - frames) * 2 math teleported the pic 32px down on frame
|
||||||
|
-- one once the budget was shortened from 30 to 14 frames.
|
||||||
|
local faintBattle = newBattle()
|
||||||
|
faintBattle.fx = { faint = { battler = faintBattle.enemy,
|
||||||
|
frames = Timing.FAINT_SLIDE } }
|
||||||
|
T.eq(faintBattle:fxFaintOffset(faintBattle.enemy, 1), 0,
|
||||||
|
"the faint slide starts at offset 0 (#671)")
|
||||||
|
faintBattle.fx.faint.frames = Timing.FAINT_SLIDE - 7
|
||||||
|
T.eq(faintBattle:fxFaintOffset(faintBattle.enemy, 1),
|
||||||
|
7 * Timing.FAINT_SLIDE_STEP,
|
||||||
|
"the slide sinks one 8px row per step")
|
||||||
|
faintBattle.fx.faint.frames = 1
|
||||||
|
T.eq(faintBattle:fxFaintOffset(faintBattle.enemy, 1),
|
||||||
|
(Timing.FAINT_SLIDE - 1) * Timing.FAINT_SLIDE_STEP,
|
||||||
|
"the slide reaches 52px by the last visible frame")
|
||||||
|
T.eq(Timing.FAINT_SLIDE * Timing.FAINT_SLIDE_STEP, 56,
|
||||||
|
"and covers the full 7-row pic height over the whole budget")
|
||||||
|
|
||||||
T.eq(Timing.MOVE_STATUS_OR_MISS, 30,
|
T.eq(Timing.MOVE_STATUS_OR_MISS, 30,
|
||||||
"a status move or a miss holds DelayFrames 30 before its text")
|
"a status move or a miss holds DelayFrames 30 before its text")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
-- UI LAYOUT (save.options.uiLayout): "centered" keeps every element where it
|
||||||
|
-- was drawn in the 160x144 canvas, so the letterbox centres the whole screen
|
||||||
|
-- the way the port composed it before edge docking existed. "dynamic" opts
|
||||||
|
-- into docking: the dialogue box to the window's bottom edge, the START menu
|
||||||
|
-- to its top right.
|
||||||
|
--
|
||||||
|
-- Centered is the DEFAULT. Docking is a real change to where screen
|
||||||
|
-- furniture sits, so it is opt-in rather than something a player has to
|
||||||
|
-- discover and turn off.
|
||||||
|
--
|
||||||
|
-- One gate, at Renderer:setUIAnchor, so the switch covers the dialogue box,
|
||||||
|
-- its YES/NO, the START menu and anything added later without any of them
|
||||||
|
-- knowing the option exists.
|
||||||
|
-- luajit tests/engine/ui_layout_option.lua
|
||||||
|
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.modkit")
|
||||||
|
local Game = require("src.core.Game")
|
||||||
|
local Renderer = require("src.render.Renderer")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------- the default
|
||||||
|
|
||||||
|
T.eq(SaveData.newGame().options.uiLayout, "centered",
|
||||||
|
"a new game starts centered, not docked")
|
||||||
|
|
||||||
|
-- ------------------------------------------------------ reading the option
|
||||||
|
|
||||||
|
-- Only the explicit "dynamic" switches docking on. Everything else means
|
||||||
|
-- centered, which is what makes this safe for a save written before the
|
||||||
|
-- option existed: the key is simply absent and the player keeps the layout
|
||||||
|
-- they already had.
|
||||||
|
T.eq(Game.dynamicUI({ options = { uiLayout = "dynamic" } }), true,
|
||||||
|
"DYNAMIC turns edge docking on")
|
||||||
|
T.eq(Game.dynamicUI({ options = { uiLayout = "centered" } }), false,
|
||||||
|
"CENTERED leaves it off")
|
||||||
|
T.eq(Game.dynamicUI({ options = {} }), false,
|
||||||
|
"a save from before the option existed is centered")
|
||||||
|
T.eq(Game.dynamicUI({}), false, "a save with no options at all is centered")
|
||||||
|
T.eq(Game.dynamicUI(nil), false, "and no save at all is centered")
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------- the gate
|
||||||
|
|
||||||
|
local function anchorsAfter(opts)
|
||||||
|
Renderer.uiAnchors = nil
|
||||||
|
Renderer.uiAnchorHold = opts.hold or false
|
||||||
|
Renderer.uiCentered = opts.centered or false
|
||||||
|
-- the dialogue box's own declaration (TextBox:draw)
|
||||||
|
Renderer:setUIAnchor(0, 96, 160, 48, "bottom")
|
||||||
|
-- and the START menu's (Menu:draw, anchor "topright")
|
||||||
|
Renderer:setUIAnchor(80, 0, 80, 88, "topright")
|
||||||
|
local n = #(Renderer.uiAnchors or {})
|
||||||
|
Renderer.uiAnchors, Renderer.uiAnchorHold, Renderer.uiCentered =
|
||||||
|
nil, false, false
|
||||||
|
return n
|
||||||
|
end
|
||||||
|
|
||||||
|
T.eq(anchorsAfter({ centered = true }), 0,
|
||||||
|
"CENTERED: neither the dialogue box nor the START menu leaves the canvas")
|
||||||
|
T.eq(anchorsAfter({ centered = false }), 2,
|
||||||
|
"DYNAMIC: both dock to the window edge")
|
||||||
|
|
||||||
|
-- the battle hold is unchanged by any of this -- a battle still keeps its own
|
||||||
|
-- prompts inside its screen even with DYNAMIC on (see battle_fixed_menu_scale)
|
||||||
|
T.eq(anchorsAfter({ centered = false, hold = true }), 0,
|
||||||
|
"a battle still holds the anchors while DYNAMIC is on")
|
||||||
|
T.eq(anchorsAfter({ centered = true, hold = true }), 0, "and with it off")
|
||||||
|
|
||||||
|
-- ------------------------------------------------- the scale half of it
|
||||||
|
|
||||||
|
-- CENTERED is a FIXED letterbox, so the UI must not follow the survey zoom
|
||||||
|
-- either: the box that stopped moving must not start resizing instead.
|
||||||
|
local g = love.graphics
|
||||||
|
local realDims, realPixelDims = g.getDimensions, g.getPixelDimensions
|
||||||
|
g.getDimensions = function() return 640, 576 end
|
||||||
|
g.getPixelDimensions = function() return 640, 576 end
|
||||||
|
T.eq(Renderer:fitScale(), 4, "the fixture window fits the classic surface at 4x")
|
||||||
|
|
||||||
|
local Zoom = require("src.render.Zoom")
|
||||||
|
local function scaleAt(offset, centered)
|
||||||
|
local oldOff, oldActive, oldCentered =
|
||||||
|
Zoom.offset, Renderer.worldActive, Renderer.uiCentered
|
||||||
|
-- worldActive true: a live overworld pass, the one case DYNAMIC steps down
|
||||||
|
Zoom.offset, Renderer.worldActive, Renderer.uiCentered = offset, true, centered
|
||||||
|
local s = Renderer:uiScale()
|
||||||
|
Zoom.offset, Renderer.worldActive, Renderer.uiCentered =
|
||||||
|
oldOff, oldActive, oldCentered
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
T.eq(scaleAt(0, true), 4, "CENTERED at rest is the fit scale")
|
||||||
|
T.eq(scaleAt(-2, true), 4, "CENTERED zoomed out is STILL the fit scale")
|
||||||
|
T.eq(scaleAt(0, false), 4, "DYNAMIC at rest matches it")
|
||||||
|
T.eq(scaleAt(-2, false), 2, "DYNAMIC zoomed out steps the UI down, as before")
|
||||||
|
|
||||||
|
g.getDimensions, g.getPixelDimensions = realDims, realPixelDims
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------- the row
|
||||||
|
|
||||||
|
-- fixture data, not Data:load(): this tier runs ROM-free in CI, so a real
|
||||||
|
-- load has no data/generated/ to read and takes the suite down with it
|
||||||
|
local OptionsMenu = require("src.ui.OptionsMenu")
|
||||||
|
local Data = T.fixtures.load()
|
||||||
|
require("src.render.Font").load(Data)
|
||||||
|
|
||||||
|
local game = { data = Data, save = SaveData.newGame(),
|
||||||
|
stack = { states = {}, push = function() end,
|
||||||
|
pop = function() end, top = function() end } }
|
||||||
|
local menu = OptionsMenu.new(game)
|
||||||
|
local row
|
||||||
|
for _, r in ipairs(menu.rows) do
|
||||||
|
if r.id == "uiLayout" then row = r end
|
||||||
|
end
|
||||||
|
T.check(row ~= nil, "OPTIONS carries a UI LAYOUT row")
|
||||||
|
T.eq(row.value(game), "CENTERED", "and it opens on CENTERED")
|
||||||
|
row.step(game, 1)
|
||||||
|
T.eq(game.save.options.uiLayout, "dynamic", "stepping it turns docking on")
|
||||||
|
T.eq(row.value(game), "DYNAMIC", "and the row says so")
|
||||||
|
row.step(game, 1)
|
||||||
|
T.eq(game.save.options.uiLayout, "centered", "stepping again returns to it")
|
||||||
|
|
||||||
|
T.finish("ui layout option")
|
||||||
@@ -779,8 +779,9 @@ check(oak.demoSpecies == "NIDORINO" and oak.nameLen == 7,
|
|||||||
|
|
||||||
-- ------- intro.oak_speech.build
|
-- ------- intro.oak_speech.build
|
||||||
local vanillaSteps = OakSpeech.defaultSteps(oak)
|
local vanillaSteps = OakSpeech.defaultSteps(oak)
|
||||||
check(#vanillaSteps == 9, "vanilla speech has nine steps")
|
check(#vanillaSteps == 11, "vanilla speech has eleven steps")
|
||||||
check(vanillaSteps[1].id == "oak_welcome" and vanillaSteps[9].id == "shrink",
|
check(vanillaSteps[1].id == "oak_welcome"
|
||||||
|
and vanillaSteps[#vanillaSteps].id == "shrink",
|
||||||
"vanilla speech anchors start and end")
|
"vanilla speech anchors start and end")
|
||||||
|
|
||||||
hooks:wrap("intro.oak_speech.build", function(nextFn, steps, speech)
|
hooks:wrap("intro.oak_speech.build", function(nextFn, steps, speech)
|
||||||
@@ -799,7 +800,7 @@ hooks:removeOwner("fixture")
|
|||||||
|
|
||||||
hooks:wrap("intro.oak_speech.build", function() return 42 end, 0, "bad")
|
hooks:wrap("intro.oak_speech.build", function() return 42 end, 0, "bad")
|
||||||
built = oak:buildSteps()
|
built = oak:buildSteps()
|
||||||
check(#built == 9 and built[1].id == "oak_welcome",
|
check(#built == #vanillaSteps and built[1].id == "oak_welcome",
|
||||||
"a non-table intro.oak_speech.build result degrades to vanilla")
|
"a non-table intro.oak_speech.build result degrades to vanilla")
|
||||||
check(logged("intro.oak_speech.build returned"),
|
check(logged("intro.oak_speech.build returned"),
|
||||||
"the intro build degrade is logged")
|
"the intro build degrade is logged")
|
||||||
|
|||||||
@@ -34,6 +34,16 @@ Game.stack = StateStack; StateStack:init()
|
|||||||
Game.save = SaveData.newGame()
|
Game.save = SaveData.newGame()
|
||||||
require("src.render.Font").load(Data)
|
require("src.render.Font").load(Data)
|
||||||
|
|
||||||
|
-- spy on Sound.play so the starter-received jingle beat is observable
|
||||||
|
-- without real audio (parity_C does the same for its arrival SFX)
|
||||||
|
local Sound = require("src.core.Sound")
|
||||||
|
local realSoundPlay = Sound.play
|
||||||
|
local played = {}
|
||||||
|
Sound.play = function(data, name)
|
||||||
|
played[#played + 1] = name
|
||||||
|
return realSoundPlay(data, name)
|
||||||
|
end
|
||||||
|
|
||||||
-- pumps a script coroutine to completion; pressFn returns the Input.pressed
|
-- pumps a script coroutine to completion; pressFn returns the Input.pressed
|
||||||
-- table for this frame (default: mash A through text/ask/naming)
|
-- table for this frame (default: mash A through text/ask/naming)
|
||||||
local function runScript(script, pressFn)
|
local function runScript(script, pressFn)
|
||||||
@@ -64,6 +74,14 @@ check(Flags.get(Game.save, "EVENT_GOT_STARTER"), "starter flag set")
|
|||||||
eq(Game.save.inventory.POKE_BALL, nil, "no POKe BALLs yet right after picking a starter")
|
eq(Game.save.inventory.POKE_BALL, nil, "no POKe BALLs yet right after picking a starter")
|
||||||
check(Game.save.party[1] and Game.save.party[1].species == "BULBASAUR",
|
check(Game.save.party[1] and Game.save.party[1].species == "BULBASAUR",
|
||||||
"starter joined the party")
|
"starter joined the party")
|
||||||
|
-- #668: OaksLabReceivedMonText carries sound_get_key_item; the jingle
|
||||||
|
-- must fire as the starter is handed over (once for the player's mon,
|
||||||
|
-- once for the rival's counter-pick)
|
||||||
|
local jingles = 0
|
||||||
|
for _, name in ipairs(played) do
|
||||||
|
if name == "Get_Key_Item" then jingles = jingles + 1 end
|
||||||
|
end
|
||||||
|
eq(jingles, 2, "starter + rival counter-pick both play the Get_Key_Item jingle (#668)")
|
||||||
-- A-mash accepts the nickname prompt and fills NamingScreen with A's
|
-- A-mash accepts the nickname prompt and fills NamingScreen with A's
|
||||||
check(Game.save.party[1].nickname == "AAAAAAAAAA",
|
check(Game.save.party[1].nickname == "AAAAAAAAAA",
|
||||||
"starter nickname prompt accepted (AskName / #137)")
|
"starter nickname prompt accepted (AskName / #137)")
|
||||||
@@ -182,4 +200,6 @@ do
|
|||||||
Game.save = realSave
|
Game.save = realSave
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Sound.play = realSoundPlay
|
||||||
|
|
||||||
S.finish()
|
S.finish()
|
||||||
|
|||||||
Reference in New Issue
Block a user