mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 16:31:05 +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:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
@@ -28,6 +29,13 @@ jobs:
|
||||
[ -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: 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
|
||||
id: build-info
|
||||
env:
|
||||
|
||||
@@ -397,12 +397,18 @@ jobs:
|
||||
date="$(date -u +"%Y-%m-%d")"
|
||||
size="$(wc -c < "$ipa" | tr -d '[:space:]')"
|
||||
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 \
|
||||
--arg version "$v" \
|
||||
--arg date "$date" \
|
||||
--arg download_url "$download_url" \
|
||||
--arg localized_description "$localized_description" \
|
||||
--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" \
|
||||
'any(.apps[] | select(.bundleIdentifier == "com.theboisclub.gen1recomp").versions[]?; .version == $version)' \
|
||||
@@ -417,15 +423,33 @@ jobs:
|
||||
fi
|
||||
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
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
uses: EndBug/add-and-commit@v10
|
||||
with:
|
||||
add: mobile/ios/app-repo.json
|
||||
default_author: github_actions
|
||||
message: "chore(ios): update app-repo.json [skip ci]"
|
||||
env:
|
||||
DEPLOY_KEY: ${{ secrets.RELEASE_DEPLOY_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
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
|
||||
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
run: |
|
||||
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)
|
||||
return {
|
||||
{ "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)
|
||||
{ "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
|
||||
-- StarterDex ahead of OaksLabYouWant...Text). StarterDex temporarily
|
||||
-- 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
|
||||
-- inside give_pokemon). Show the received text first so the
|
||||
-- nickname prompt follows "you got X", matching Gen1.
|
||||
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 8
|
||||
{ "give_pokemon", species, 5 }, -- 9
|
||||
{ "set_flag", "EVENT_GOT_STARTER" }, -- 10
|
||||
{ "set_flag", choseFlag }, -- 11
|
||||
-- The received text carries sound_get_key_item (OaksLab.asm
|
||||
-- OaksLabReceivedMonText); the jingle plays as the box opens
|
||||
-- (same beat as the Yellow port's starter, #668).
|
||||
{ "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
|
||||
-- them later, at OaksLabOak1Text's .give_poke_balls beat once the
|
||||
-- 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)
|
||||
{ "move_npc_to", 1, rivalBallX, 4 }, -- 13
|
||||
{ "face_object", 1, "up" }, -- 14
|
||||
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 15
|
||||
{ "hide_object", "OAKS_LAB", rivalBall }, -- 16
|
||||
{ "move_npc_to", 1, rivalBallX, 4 }, -- 14
|
||||
{ "face_object", 1, "up" }, -- 15
|
||||
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 16
|
||||
{ "hide_object", "OAKS_LAB", rivalBall }, -- 17
|
||||
{ "play_sound", "Get_Key_Item" }, -- 18 (sound_get_key_item)
|
||||
{ "show_text", "_OaksLabRivalReceivedMonText",
|
||||
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
|
||||
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
|
||||
or "BULBASAUR" } }, -- 17
|
||||
{ "jump", "end" }, -- 18
|
||||
{ "jump", "end" }, -- 19 (spacer)
|
||||
or "BULBASAUR" } }, -- 19
|
||||
{ "jump", "end" }, -- 20
|
||||
{ "jump", "end" }, -- 21 (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
|
||||
{ "face_object", 5, "down" }, -- 22
|
||||
{ "show_text", "That's PROF.OAK's\nlast Pokémon!" }, -- 23
|
||||
-- OaksLabLastMonScript ends at TextScriptEnd; the port used to fall
|
||||
-- through into the pre-pick line below (#601 remnant, reported on #600)
|
||||
{ "jump", "end" }, -- 22
|
||||
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 23
|
||||
{ "jump", "end" }, -- 24
|
||||
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 25
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -11,7 +11,15 @@
|
||||
"localizedDescription": "Gen1Recomp - A native Lua / LÖVE2D recreation of Gen 1 Poke",
|
||||
"tintColor": "3b5ca8",
|
||||
"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
|
||||
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
|
||||
say "stamping engine version $VERSION into game.love"
|
||||
local stamp_dir
|
||||
stamp_dir="$(mktemp -d)"
|
||||
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"
|
||||
(cd "$stamp_dir" && zip -q "$LOVE_FILE" src/core/Version.lua)
|
||||
local version_re
|
||||
@@ -464,6 +453,86 @@ print("patched project.pbxproj")
|
||||
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
|
||||
# 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
|
||||
@@ -543,6 +612,7 @@ run_xcodebuild() {
|
||||
MARKETING_VERSION="$marketing_version"
|
||||
CURRENT_PROJECT_VERSION="$project_version"
|
||||
ONLY_ACTIVE_ARCH=NO
|
||||
DISABLE_MANUAL_TARGET_ORDER_BUILD_WARNING=YES
|
||||
)
|
||||
|
||||
if ! $DEVICE; then
|
||||
@@ -702,6 +772,7 @@ python3 "$IOS_DIR/patch_love_src.py" || fail "patch_love_src.py failed"
|
||||
ensure_manifests
|
||||
pack_game_love
|
||||
ensure_game_love_in_xcode
|
||||
suppress_love_dependency_warnings
|
||||
|
||||
if $PACKAGE_ONLY; then
|
||||
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 = {}
|
||||
BattleState.__index = BattleState
|
||||
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
|
||||
-- window reads as one continuous battle screen (no black bars).
|
||||
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)
|
||||
markSeen(game, species)
|
||||
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
|
||||
self.introText = Strings("Wild %s\nappeared!", self.enemy.name)
|
||||
self.introText = self:romText("_WildMonAppearedText", "Wild %s\nappeared!", self.enemy.name)
|
||||
end
|
||||
return self
|
||||
end
|
||||
@@ -753,7 +812,7 @@ function BattleState:queueScopeReveal()
|
||||
or Strings("SILPH SCOPE\nunveiled the\vGHOST's identity!"))
|
||||
self:act(function() self.ghostReveal = { t = 0 } end)
|
||||
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))
|
||||
end
|
||||
|
||||
@@ -1292,7 +1351,7 @@ function BattleState:sendOutText(name)
|
||||
if pct >= 70 then return Strings("Go! %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
|
||||
return Strings("The enemy's weak!\nGet'm! %s!", name)
|
||||
return self:romText("_EnemysWeakText", "The enemy's weak!\nGet'm! %s!", name)
|
||||
end
|
||||
|
||||
-- audio/play_battle_music.asm: gym leaders (wGymLeaderNo) get the
|
||||
@@ -1866,11 +1925,11 @@ function BattleState:update(dt)
|
||||
end
|
||||
local mv = moves[self.moveIndex]
|
||||
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.afterQueue = "menu"
|
||||
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.afterQueue = "menu"
|
||||
else
|
||||
@@ -1920,7 +1979,7 @@ function BattleState:resolveMimic(user, target, move, moveInst)
|
||||
table.insert(self.queue, self.nextInsert, { wait = 50 })
|
||||
if target.invulnerable
|
||||
or not self:accuracyRoll(move, user, target) then
|
||||
self:sayNext(Strings("But, it failed!"))
|
||||
self:sayNext(self:romText("_ButItFailedText", "But, it failed!"))
|
||||
return
|
||||
end
|
||||
local slots = {}
|
||||
@@ -1930,7 +1989,7 @@ function BattleState:resolveMimic(user, target, move, moveInst)
|
||||
if #slots == 0 then
|
||||
-- .getRandomMove rerolls empty slots forever; a moveless target
|
||||
-- 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
|
||||
end
|
||||
if user.isPlayer and self.kind ~= "link" then
|
||||
@@ -1991,7 +2050,7 @@ function BattleState:applyMimic(user, target, moveInst, slot)
|
||||
entry.mimic = true
|
||||
self:animNext("MIMIC", user.isPlayer)
|
||||
-- _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))
|
||||
end
|
||||
|
||||
@@ -3053,7 +3112,7 @@ function BattleState:executeAction(user, target, action)
|
||||
-- 3392): sleep/freeze/held/flinch keep the mon recharging next turn
|
||||
if self:preRechargeChecks(user, target) then return end
|
||||
user.mustRecharge = nil
|
||||
self:sayNext(Strings("%s\nmust recharge!", displayName(user)))
|
||||
self:sayNext(self:romText("_MustRechargeText", "%s\nmust recharge!", displayName(user)))
|
||||
return
|
||||
end
|
||||
if action.special == "bound" then
|
||||
@@ -3098,8 +3157,8 @@ function BattleState:statusOnomatopoeia(user, kind)
|
||||
anim = isPlayer and "CONF_PLAYER_ANIM" or "CONF_ANIM"
|
||||
end
|
||||
local text = kind == "sleep"
|
||||
and Strings("%s\nis fast asleep!", displayName(user))
|
||||
or Strings("%s\nis confused!", displayName(user))
|
||||
and self:romText("_FastAsleepText", "%s\nis fast asleep!", displayName(user))
|
||||
or self:romText("_IsConfusedText", "%s\nis confused!", displayName(user))
|
||||
if kind == "sleep" and isPlayer then
|
||||
self:animNext(anim, isPlayer)
|
||||
self:sayNext(text)
|
||||
@@ -3138,18 +3197,18 @@ function BattleState:preRechargeChecks(user, target)
|
||||
user.sleepTurns = (user.sleepTurns or 1) - 1
|
||||
if user.sleepTurns <= 0 then
|
||||
mon.status = nil
|
||||
self:sayNext(Strings("%s\nwoke up!", displayName(user)))
|
||||
self:sayNext(self:romText("_WokeUpText", "%s\nwoke up!", displayName(user)))
|
||||
else
|
||||
self:statusOnomatopoeia(user, "sleep")
|
||||
end
|
||||
return true
|
||||
end
|
||||
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
|
||||
end
|
||||
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
|
||||
end
|
||||
if user.flinched then
|
||||
@@ -3157,7 +3216,7 @@ function BattleState:preRechargeChecks(user, target)
|
||||
-- player recharges, so the flinch eats the recharge turn and the
|
||||
-- flag survives (the Hyper Beam flinch glitch)
|
||||
user.flinched = false
|
||||
self:sayNext(Strings("%s\nflinched!", displayName(user)))
|
||||
self:sayNext(self:romText("_FlinchedText", "%s\nflinched!", displayName(user)))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
@@ -3178,7 +3237,7 @@ function BattleState:statusInterrupt(user, target)
|
||||
{ id = "CONFUSED", power = 40, type = "NORMAL", accuracy = 100 },
|
||||
{ rng = self.rng, forceCrit = false, typeless = true,
|
||||
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:applyDamage(user, dmg)
|
||||
if user.mon.hp <= 0 then self:onFaint(user) end
|
||||
@@ -3266,7 +3325,7 @@ function BattleState:performMove(user, target, moveInst, isCalled)
|
||||
|
||||
self.moveAnimRow = nil
|
||||
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
|
||||
-- damage path attaches the target's hit blink to this row so 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
|
||||
-- after the effect lands; a miss skips it
|
||||
self:cancelMoveAnim()
|
||||
self:sayNext(Strings("%s's\nattack missed!", displayName(user)))
|
||||
self:sayNext(self:romText("_AttackMissedText", "%s's\nattack missed!", displayName(user)))
|
||||
return
|
||||
end
|
||||
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
|
||||
MoveEffects.warnUnknown(move.effect)
|
||||
self:cancelMoveAnim()
|
||||
self:sayNext(Strings("But, it failed!"))
|
||||
self:sayNext(self:romText("_ButItFailedText", "But, it failed!"))
|
||||
return
|
||||
end
|
||||
|
||||
@@ -3380,7 +3439,7 @@ function BattleState:performMove(user, target, moveInst, isCalled)
|
||||
end
|
||||
|
||||
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
|
||||
-- then jumps to GetPlayerAnimationType, so the trapping move's full
|
||||
-- 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)))
|
||||
return
|
||||
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
|
||||
user.bideTurns, user.bideDamage = nil, nil
|
||||
if dmg <= 0 then
|
||||
self:cancelMoveAnim()
|
||||
self:sayNext(Strings("But, it failed!"))
|
||||
self:sayNext(self:romText("_ButItFailedText", "But, it failed!"))
|
||||
return
|
||||
end
|
||||
-- .UnleashEnergy (core.asm:3501-3529) re-points wPlayerMoveNum at BIDE
|
||||
@@ -3434,9 +3493,9 @@ function BattleState:applyDamage(target, dmg)
|
||||
target.substituteHP = target.substituteHP - dmg
|
||||
if target.substituteHP <= 0 then
|
||||
target.substituteHP = nil
|
||||
self:sayNext(Strings("%s's\nSUBSTITUTE broke!", displayName(target)))
|
||||
self:sayNext(self:romText("_SubstituteBrokeText", "%s's\nSUBSTITUTE broke!", displayName(target)))
|
||||
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
|
||||
return dmg
|
||||
end
|
||||
@@ -3448,7 +3507,7 @@ function BattleState:applyDamage(target, dmg)
|
||||
end
|
||||
if target.rageMove and dealt > 0 then
|
||||
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
|
||||
return dealt
|
||||
end
|
||||
@@ -3776,7 +3835,7 @@ function BattleState:enemyMonFainted()
|
||||
-- scripted battles that print their own follow-up leave it nil.
|
||||
self:actNext(function() self:playVictoryMusic() end)
|
||||
-- _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:actNext(function()
|
||||
self.showEnemyTrainer = self.trainerPic ~= nil
|
||||
@@ -3803,7 +3862,7 @@ function BattleState:enemyMonFainted()
|
||||
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
|
||||
self.result = "win"
|
||||
self.afterQueue = "finish"
|
||||
@@ -3819,7 +3878,7 @@ function BattleState:learnMove(mon, moveId)
|
||||
if #mon.moves < 4 then
|
||||
table.insert(mon.moves, { id = moveId, pp = mdef.pp })
|
||||
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))
|
||||
return
|
||||
end
|
||||
@@ -3901,11 +3960,11 @@ function BattleState:playerMonFainted()
|
||||
local pSpd = (game.save.party[1].stats or { speed = 0 }).speed or 0
|
||||
if self:runRoll(pSpd, TurnOrder.effectiveSpeed(self.enemy)) then
|
||||
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.afterQueue = "finish"
|
||||
else
|
||||
self:say(Strings("Can't escape!"))
|
||||
self:say(self:romText("_CantEscapeText", "Can't escape!"))
|
||||
end
|
||||
end)
|
||||
end)
|
||||
@@ -3925,7 +3984,7 @@ function BattleState:openReplacementMenu()
|
||||
forceSwitch = true,
|
||||
onSwitch = function(mon)
|
||||
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
|
||||
end
|
||||
self:restoreMimicked(self.player)
|
||||
@@ -3969,7 +4028,7 @@ function BattleState:safariAction(choice)
|
||||
|
||||
if choice == "run" then
|
||||
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.afterQueue = "finish"
|
||||
return
|
||||
@@ -4006,12 +4065,12 @@ function BattleState:safariAction(choice)
|
||||
end
|
||||
|
||||
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.baitFactor = math.min(255, self.baitFactor + self.rng(1, 5))
|
||||
self.escapeFactor = 0
|
||||
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.escapeFactor = math.min(255, self.escapeFactor + self.rng(1, 5))
|
||||
self.baitFactor = 0
|
||||
@@ -4027,13 +4086,13 @@ end
|
||||
function BattleState:safariEnemyTurn()
|
||||
if self.baitFactor > 0 then
|
||||
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
|
||||
self.escapeFactor = self.escapeFactor - 1
|
||||
if self.escapeFactor == 0 then
|
||||
self.safariCatchRate = self.enemy.def.catchRate
|
||||
end
|
||||
self:sayNext(Strings("Wild %s\nis angry!", self.enemy.name))
|
||||
self:sayNext(self:romText("_SafariZoneAngryText", "Wild %s\nis angry!", self.enemy.name))
|
||||
end
|
||||
self:act(function()
|
||||
local speed = self.enemy.curStats.speed % 256
|
||||
@@ -4049,7 +4108,7 @@ function BattleState:safariEnemyTurn()
|
||||
fled = self.rng(0, 255) < b
|
||||
end
|
||||
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()
|
||||
require("src.core.Sound").play(self.data, "Run")
|
||||
startPicKind(self:picFxFor(self.enemy), "slideOff")
|
||||
@@ -4116,11 +4175,11 @@ function BattleState:tryRun()
|
||||
TurnOrder.effectiveSpeed(self.enemy))
|
||||
if escaped then
|
||||
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.afterQueue = "finish"
|
||||
else
|
||||
self:say(Strings("Can't escape!"))
|
||||
self:say(self:romText("_CantEscapeText", "Can't escape!"))
|
||||
self:act(function()
|
||||
self:executeAction(self.enemy, self.player, self:enemyAction())
|
||||
end)
|
||||
@@ -4157,9 +4216,9 @@ function BattleState:ballMissMessage(shakes)
|
||||
elseif shakes == 1 then
|
||||
return t._ItemUseBallText02 or Strings("Darn! The POKéMON\nbroke free!")
|
||||
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
|
||||
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
|
||||
|
||||
-- AskName (engine/menus/naming_screen.asm): ClearSprites, wild field blank,
|
||||
@@ -4170,7 +4229,7 @@ function BattleState:askNicknameUI(mon, displayName)
|
||||
self.lockedBall = nil
|
||||
self.blankForAskName = true
|
||||
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
|
||||
if label then
|
||||
-- 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
|
||||
-- still wIsInBattle == 1, and this port models both as kind == "wild".
|
||||
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))
|
||||
end
|
||||
self:act(function()
|
||||
@@ -4321,9 +4380,9 @@ function BattleState:throwBall(ball)
|
||||
end)
|
||||
self:animNext("BLOCKBALL_ANIM", true)
|
||||
self:sayNext(t._ThrowBallAtTrainerMonText1
|
||||
or Strings("The trainer\nblocked the BALL!"))
|
||||
or self:romText("_ThrowBallAtTrainerMonText1", "The trainer\nblocked the BALL!"))
|
||||
self:sayNext(t._ThrowBallAtTrainerMonText2
|
||||
or Strings("Don't be a thief!"))
|
||||
or self:romText("_ThrowBallAtTrainerMonText2", "Don't be a thief!"))
|
||||
self:act(function()
|
||||
self:executeAction(self.enemy, self.player, self:enemyAction())
|
||||
end)
|
||||
@@ -4386,7 +4445,7 @@ function BattleState:openParty()
|
||||
if mon == self.player.mon then
|
||||
self:say(Strings("%s is\nalready out!", self.player.name))
|
||||
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
|
||||
self:resolveSwitch(mon)
|
||||
end
|
||||
@@ -4412,7 +4471,7 @@ end
|
||||
function BattleState:finish()
|
||||
if self.payDay and self.result == "win" then
|
||||
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.afterQueue = "finish"
|
||||
self.phase = "messages"
|
||||
@@ -4542,11 +4601,15 @@ end
|
||||
-- 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
|
||||
-- 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)
|
||||
local fx = self.fx
|
||||
if self:fxFaintActive(battler) then
|
||||
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
|
||||
return 0
|
||||
end
|
||||
|
||||
@@ -315,6 +315,15 @@ end
|
||||
-- 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
|
||||
-- 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)
|
||||
for i = #(stack and stack.states or {}), 1, -1 do
|
||||
local state = stack.states[i]
|
||||
@@ -415,6 +424,13 @@ function Game:draw()
|
||||
Renderer.uiWorldHold = Renderer.battleDim ~= nil
|
||||
-- ...and a battle keeps its dialogue box and YES/NO inside its own screen
|
||||
-- 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:beginFrame(worldBelow)
|
||||
for i = drawFrom, #self.stack.states do
|
||||
|
||||
@@ -228,6 +228,15 @@ function SaveData.defaultOptions()
|
||||
-- "black" = plain black bars, "world" = the frozen overworld showing
|
||||
-- through, dimmed. See BattleState:bgMode.
|
||||
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",
|
||||
-- 0-7 like the GB's NR50 master volume
|
||||
musicVol = 7,
|
||||
|
||||
@@ -129,6 +129,7 @@ Timing.NO_MOVES_LEFT = 60 -- core.asm:2753-2754
|
||||
Timing.TRAINER_VICTORY = 40 -- core.asm:940-941
|
||||
Timing.PLAYER_BLACKOUT = 40 -- core.asm:1143-1144
|
||||
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
|
||||
|
||||
-- HP bar (engine/gfx/hp_bar.asm) ---------------------------------------------
|
||||
|
||||
@@ -142,6 +142,13 @@ end
|
||||
function Renderer:uiScale()
|
||||
local S = self:fitScale()
|
||||
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
|
||||
-- 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
|
||||
@@ -630,6 +637,14 @@ end
|
||||
-- pixels, and consumed by endFrame this frame only.
|
||||
-- anchor: "bottom" | "topright" | "topleft" | "bottomright"
|
||||
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
|
||||
-- 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.
|
||||
|
||||
@@ -20,6 +20,16 @@ local OakSpeech = {}
|
||||
OakSpeech.__index = OakSpeech
|
||||
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
|
||||
-- sprite in from the right. Both play out before the beat's text prints.
|
||||
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!"),
|
||||
_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?"),
|
||||
_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)
|
||||
@@ -152,6 +164,14 @@ function OakSpeech.defaultSteps(speech)
|
||||
presetsWho = "player",
|
||||
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",
|
||||
kind = "say",
|
||||
@@ -166,6 +186,12 @@ function OakSpeech.defaultSteps(speech)
|
||||
presetsWho = "rival",
|
||||
presetsFallback = { "BLUE", "GARY", "JOHN" },
|
||||
},
|
||||
{
|
||||
-- HisNameIsText, the rival's counterpart to the confirmation above
|
||||
id = "confirm_rival_name",
|
||||
kind = "say",
|
||||
textKey = "_HisNameIsText",
|
||||
},
|
||||
{
|
||||
id = "legend",
|
||||
kind = "say",
|
||||
@@ -611,6 +637,15 @@ function OakSpeech:draw()
|
||||
love.graphics.draw(self.walkSheet, self.walkQuad, 64, 60)
|
||||
end
|
||||
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)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
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]
|
||||
return true
|
||||
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"),
|
||||
value = function(g) return rulesetName(g) end,
|
||||
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
|
||||
-- balanced, so a call wrapped across lines counts as covered. A per-line
|
||||
-- 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 out, i, n = {}, 1, #body
|
||||
while i <= n do
|
||||
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
|
||||
out[#out + 1] = body:sub(i, s - 1)
|
||||
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,
|
||||
"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)")
|
||||
-- the pokered beat also turns Oak to face the player: find the
|
||||
-- face_object row in the leftover-ball path (content-based, so a jingle
|
||||
-- row added for #668 doesn't shift the hard-coded index)
|
||||
local oakFaceRow
|
||||
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
|
||||
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")
|
||||
|
||||
-- ---- 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({
|
||||
"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")
|
||||
local lastMon
|
||||
if script then
|
||||
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
|
||||
|
||||
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
|
||||
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,
|
||||
"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
|
||||
local vanillaSteps = OakSpeech.defaultSteps(oak)
|
||||
check(#vanillaSteps == 9, "vanilla speech has nine steps")
|
||||
check(vanillaSteps[1].id == "oak_welcome" and vanillaSteps[9].id == "shrink",
|
||||
check(#vanillaSteps == 11, "vanilla speech has eleven steps")
|
||||
check(vanillaSteps[1].id == "oak_welcome"
|
||||
and vanillaSteps[#vanillaSteps].id == "shrink",
|
||||
"vanilla speech anchors start and end")
|
||||
|
||||
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")
|
||||
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")
|
||||
check(logged("intro.oak_speech.build returned"),
|
||||
"the intro build degrade is logged")
|
||||
|
||||
@@ -34,6 +34,16 @@ Game.stack = StateStack; StateStack:init()
|
||||
Game.save = SaveData.newGame()
|
||||
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
|
||||
-- table for this frame (default: mash A through text/ask/naming)
|
||||
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")
|
||||
check(Game.save.party[1] and Game.save.party[1].species == "BULBASAUR",
|
||||
"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
|
||||
check(Game.save.party[1].nickname == "AAAAAAAAAA",
|
||||
"starter nickname prompt accepted (AskName / #137)")
|
||||
@@ -182,4 +200,6 @@ do
|
||||
Game.save = realSave
|
||||
end
|
||||
|
||||
Sound.play = realSoundPlay
|
||||
|
||||
S.finish()
|
||||
|
||||
Reference in New Issue
Block a user