Pulled every open Gold bug (bodies, comments, screenshots/videos), matched them against the port and ../pokegold, then split them so each fixer owns a disjoint file set. Duplicates collapsed: #1164 and #1188 are the same Radio Tower stair softlock. #1150 and #1190 are the same Gold rebind freeze. Enhancements (#1161, #1130, #1112, #1100) were left out. Battle 1 src/battle/gen2/*, src/ui/gen2/BattleState.lua #1168 multi-hit always 2 #1180 DV / stats #1185 learn-move text order after a trainer fight #1152 no "Use next POKéMON?" / run after a wild faint Overworld 2 src/world/gen2/*, trade anim, hidden items #1184 Kurt blocking Slowpoke Well #1164 / #1188 Radio Tower gentleman on the stairs #1173 Rock Smash rocks walk left each visit #1172 last item name used Rock Smash #1167 Ilex Forest hidden items #1165 trade movie + ledge hop still missing (#1121 / #1126) UI 3 naming, party, mart, pokegear #1166 grey nickname screen #1162 TM/HM missing ABLE / NOT ABLE #1169 underground herb lady whites out the map #1151 Pokegear black player icon + extra A/B on the phone card Platform 4 Gold options, save slots, pack, mod tab #1150 / #1190 controls freeze #1178 no vibration row #1177 cannot move Gold touch controls #1107 SAVE with no launcher slot #1145 Gold mod tab still lists Gen 1-only mods #1192 RGXX missing tools/rom_manifest_gold.json Drivers only for the progress / data-loss ones (faint-run, multi-hit, Kurt, Radio Tower, Rock Smash shift, herb shop, save slot). No new unit tests. New comments are pokegold file.asm:line only. A verifier agent will get this same list once the four finish and check that the diffs actually fix the reports. Verifier checked the batch against pokegold. Almost everything landed. I closed the two leftovers that were still real bugs: #1185: the active mon no longer reprints GrewToLevel (engine/battle/core.asm:7044). Bench mons still print it, then the stats box, then the learn-move line. #1180: the party list recalcs stats on open, same as Summary. Gold boot now applies options.touchControls / haptics from the Gold options block, not the shared Red pad. # Verdict 1168 multi-hit FIXED 1180 DVs / stats FIXED 1185 learn-move order FIXED (was partial) 1152 run after faint FIXED 1184 Kurt FIXED 1164 / 1188 Radio Tower FIXED 1173 rocks slide FIXED 1172 Rock Smash name FIXED 1167 Ilex hidden items SKIP-OK (already in the Gold cache) 1165 trade + ledge FIXED 1166 nickname color FIXED 1162 TM ABLE / NOT ABLE FIXED 1169 herb shop white-out FIXED 1151 Pokegear FIXED 1150 / 1190 rebind freeze FIXED 1178 vibration FIXED 1177 touch layout FIXED 1107 save slot FIXED 1145 mod tab filter FIXED 1192 RGXX Gold manifest FIXED Nothing is committed. Drivers, from the repo root: POKEPORT_GAME=gold POKEPORT_TOUCH=0 POKEPORT_DRIVER=tests/drivers/multihit_bug1168_test.lua love . Fury Attack should hit 2-5 times. Always twice is still broken. POKEPORT_GAME=gold POKEPORT_TOUCH=0 POKEPORT_DRIVER=tests/drivers/use_next_mon_bug1152_test.lua love . After the lead faints: "Use next POKéMON?" YES opens the party. NO/B tries to run. POKEPORT_GAME=gold POKEPORT_TOUCH=0 POKEPORT_DRIVER=tests/drivers/kurt_well_bug1184_test.lua love . Kurt at the well entrance (16, 14), not on the inner path. POKEPORT_GAME=gold POKEPORT_TOUCH=0 POKEPORT_DRIVER=tests/drivers/radiotower_softlock_bug1164_test.lua love . Gentleman in the 5F office (3, 6). Stairs at (12, 0) clear. POKEPORT_GAME=gold POKEPORT_TOUCH=0 POKEPORT_DRIVER=tests/drivers/rocksmash_shift_bug1173_test.lua love . Route 40 rocks stay put. A slide one cell left is the old bug. POKEPORT_GAME=gold POKEPORT_TOUCH=0 POKEPORT_DRIVER=tests/drivers/herbshop_bug1169_test.lua love . Herb shop intro over the Underground map, not a white screen. POKEPORT_GAME=gold POKEPORT_IDENTITY=gold-bug1107 POKEPORT_DRIVER=tests/drivers/gold_save_slot_bug1107_test.lua love . Look for PASS and a saves/gold/slot*.lua path. CONTINUE should see it. Worth a hand check with no driver: nickname color, a TM for ABLE/NOT ABLE, Pokegear phone B-to-close, OPTION > CONTROLS, a ledge hop, a trade movie, and on a phone OPTION > TOUCH LAYOUT / VIBRATION. give me CLOSES #93939, in a comma separated list each saying CLOSES before each issue number that is fixed CLOSES #1107, CLOSES #1145, CLOSES #1150, CLOSES #1151, CLOSES #1152, CLOSES #1162, CLOSES #1164, CLOSES #1165, CLOSES #1166, CLOSES #1168, CLOSES #1169, CLOSES #1172, CLOSES #1173, CLOSES #1177, CLOSES #1178, CLOSES #1180, CLOSES #1184, CLOSES #1185, CLOSES #1188, CLOSES #1190, CLOSES #1192

This commit is contained in:
bryanthaboi
2026-08-12 16:51:27 -04:00
parent 49d094b14d
commit 6731937841
35 changed files with 1412 additions and 116 deletions
+7 -4
View File
@@ -91,11 +91,14 @@ mkdir -p "$GAME_SRC"
(cd "$ROOT" && zip -q -9 -r "$WORK/game-payload.zip" \ (cd "$ROOT" && zip -q -9 -r "$WORK/game-payload.zip" \
main.lua conf.lua src libs data assets tools/save-editor \ main.lua conf.lua src libs data assets tools/save-editor \
tools/rom_manifest.json tools/rom_manifest_blue.json \ tools/rom_manifest.json tools/rom_manifest_blue.json \
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*') -x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
if unzip -Z1 "$WORK/game-payload.zip" \ payload_list="$(unzip -Z1 "$WORK/game-payload.zip")"
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then printf '%s\n' "$payload_list" \
fail "payload unexpectedly contains generated ROM data" | grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/' \
fi && fail "payload unexpectedly contains generated ROM data"
printf '%s\n' "$payload_list" | grep -qxF "tools/rom_manifest_gold.json" \
|| fail "payload is missing tools/rom_manifest_gold.json"
unzip -q "$WORK/game-payload.zip" -d "$GAME_SRC" unzip -q "$WORK/game-payload.zip" -d "$GAME_SRC"
rm -f "$WORK/game-payload.zip" rm -f "$WORK/game-payload.zip"
+1 -3
View File
@@ -27,8 +27,7 @@ Features intentionally added beyond the original Pokémon Red, Blue, and Yellow
A fourth game the launcher can import and play, built from pret/pokegold the same way Red/Blue/Yellow are built from pokered. Port extras beyond the cartridge: A fourth game the launcher can import and play, built from pret/pokegold the same way Red/Blue/Yellow are built from pokered. Port extras beyond the cartridge:
* **Separate Gold save file** beside the Gen 1 ones * **COLOR, zoom, tilt, GBC FX, and quick save/load**
* **COLOR, zoom, tilt, GBC FX, and quick save/load** on the same keys as Gen 1
* **UI that stays fixed while the overworld zooms** * **UI that stays fixed while the overworld zooms**
* **Border-block surrounds** for maps smaller than the screen * **Border-block surrounds** for maps smaller than the screen
* **Gold-specific launcher options** * **Gold-specific launcher options**
@@ -39,5 +38,4 @@ A fourth game the launcher can import and play, built from pret/pokegold the sam
* **Followers** for mods, plus Gen 2-only registries and hooks * **Followers** for mods, plus Gen 2-only registries and hooks
* **On-screen touch pad** and controller SELECT for registered items * **On-screen touch pad** and controller SELECT for registered items
Actual approximations, and missing original behavior are documented separately in `docs/known-differences.md`.
+51 -3
View File
@@ -27,10 +27,9 @@ APPLICATION_ID="com.theboisclub.pokemonred"
LOVE_ANDROID_VERSION="11.5a" LOVE_ANDROID_VERSION="11.5a"
NDK_VERSION="25.2.9519653" NDK_VERSION="25.2.9519653"
YELLOW_MANIFEST_RELATIVE="tools/rom_manifest_yellow.json" YELLOW_MANIFEST_RELATIVE="tools/rom_manifest_yellow.json"
# A fresh source checkout normally supplies this through Git. This URL is
# deliberately only a last resort for incomplete source exports: the manifest
# contains extraction metadata, never a ROM or extracted game data.
YELLOW_MANIFEST_URL="${YELLOW_MANIFEST_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/tools/rom_manifest_yellow.json}" YELLOW_MANIFEST_URL="${YELLOW_MANIFEST_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/tools/rom_manifest_yellow.json}"
GOLD_MANIFEST_RELATIVE="tools/rom_manifest_gold.json"
GOLD_MANIFEST_URL="${GOLD_MANIFEST_URL:-https://raw.githubusercontent.com/bryanthaboi/gen1recomp/main/tools/rom_manifest_gold.json}"
VERSION="" VERSION=""
PACKAGE_ONLY=false PACKAGE_ONLY=false
@@ -130,6 +129,54 @@ ensure_yellow_manifest() {
fail "Yellow import manifest is unavailable. Git recovery failed and could not download $YELLOW_MANIFEST_URL" fail "Yellow import manifest is unavailable. Git recovery failed and could not download $YELLOW_MANIFEST_URL"
} }
gold_manifest_is_valid() {
local path="$1"
python3 - "$path" <<'PY'
import json, pathlib, sys
try:
manifest = json.loads(pathlib.Path(sys.argv[1]).read_text())
except (OSError, ValueError):
raise SystemExit(1)
raise SystemExit(0 if manifest.get("romSha1") ==
"d8b8a3600a465308c9953dfa04f0081c05bdcb94" else 1)
PY
}
ensure_gold_manifest() {
local manifest="$ROOT/$GOLD_MANIFEST_RELATIVE"
local staged
staged="$(mktemp)"
if gold_manifest_is_valid "$manifest"; then
rm -f "$staged"
return
fi
warn "Gold import manifest is missing or invalid; recovering it before packaging"
if git -C "$ROOT" show "HEAD:$GOLD_MANIFEST_RELATIVE" > "$staged" 2>/dev/null \
&& gold_manifest_is_valid "$staged"; then
mkdir -p "$(dirname "$manifest")"
mv "$staged" "$manifest"
say "restored Gold import manifest from this checkout's Git data"
return
fi
if command -v curl >/dev/null 2>&1 \
&& curl --fail --location --retry 2 --connect-timeout 15 \
--output "$staged" "$GOLD_MANIFEST_URL" \
&& gold_manifest_is_valid "$staged"; then
mkdir -p "$(dirname "$manifest")"
mv "$staged" "$manifest"
say "downloaded Gold import manifest from the project repository"
return
fi
rm -f "$staged"
fail "Gold import manifest is unavailable. Git recovery failed and could not download $GOLD_MANIFEST_URL"
}
# --------------------------------------------------------------- branding # --------------------------------------------------------------- branding
# love-android 11.5+ reads app id / name / orientation from gradle.properties. # love-android 11.5+ reads app id / name / orientation from gradle.properties.
# Manifest still gets permission trims. Re-applied every build so refreshing # Manifest still gets permission trims. Re-applied every build so refreshing
@@ -194,6 +241,7 @@ PY
pack_game_love() { pack_game_love() {
say "packing game.love for love-android embed flavor" say "packing game.love for love-android embed flavor"
ensure_yellow_manifest ensure_yellow_manifest
ensure_gold_manifest
mkdir -p "$EMBED_ASSETS" mkdir -p "$EMBED_ASSETS"
rm -f "$LOVE_FILE" rm -f "$LOVE_FILE"
# tools/save-editor ships with the app: the launcher's Edit button on a save # tools/save-editor ships with the app: the launcher's Edit button on a save
+3
View File
@@ -357,6 +357,9 @@ cp -R "$jit_share/jit" "$APPDIR/share/$LUAJIT_SHARE_DIR/"
# --------------------------------------------------------------- branding # --------------------------------------------------------------- branding
cp "$IN/game.love" "$APPDIR/game.love" cp "$IN/game.love" "$APPDIR/game.love"
unzip -Z1 "$APPDIR/game.love" > "$WORK/love-listing.txt"
grep -qxF "tools/rom_manifest_gold.json" "$WORK/love-listing.txt" \
|| fail "game.love is missing tools/rom_manifest_gold.json"
# The .desktop's Icon= resolves against the AppDir root by basename, and # The .desktop's Icon= resolves against the AppDir root by basename, and
# .DirIcon is what appimaged and file-manager thumbnailers read. # .DirIcon is what appimaged and file-manager thumbnailers read.
cp "$IN/icon.png" "$APPDIR/$APP_NAME.png" cp "$IN/icon.png" "$APPDIR/$APP_NAME.png"
@@ -167,5 +167,7 @@ trap 'rm -rf "$temp_dir"' EXIT
unzip -p "$temp_dir/game.love" src/core/Version.lua \ unzip -p "$temp_dir/game.love" src/core/Version.lua \
| grep -Eq 'engine[[:space:]]*=[[:space:]]*"1\.2\.3"' \ | grep -Eq 'engine[[:space:]]*=[[:space:]]*"1\.2\.3"' \
|| fail "shared payload version was not stamped" || fail "shared payload version was not stamped"
grep -qxF "tools/rom_manifest_gold.json" "$temp_dir/love-listing.txt" \
|| fail "shared payload is missing tools/rom_manifest_gold.json"
say "Linux arm64 self-test passed" say "Linux arm64 self-test passed"
+14 -5
View File
@@ -217,7 +217,7 @@ function Battle.new(opts)
opts = opts or {} opts = opts or {}
local self = setmetatable({}, Battle) local self = setmetatable({}, Battle)
self.data = opts.data or {} self.data = opts.data or {}
self.random = opts.random self.random = opts.random or function(n) return rand(nil, n) end
self.party = opts.party or {} self.party = opts.party or {}
self.trainer = opts.trainer self.trainer = opts.trainer
self.save = opts.save self.save = opts.save
@@ -271,6 +271,13 @@ function Battle.new(opts)
self.enemy = self.enemyParty[self.enemyIndex] self.enemy = self.enemyParty[self.enemyIndex]
end end
for _, mon in ipairs(self.party) do
Mon.refreshStats(mon, self.data)
end
for _, mon in ipairs(self.enemyParty or {}) do
Mon.refreshStats(mon, self.data)
end
-- Battle RAM opens empty on both sides: NewBattleMonStatus and -- Battle RAM opens empty on both sides: NewBattleMonStatus and
-- NewEnemyMonStatus run at the first send-out of every battle. -- NewEnemyMonStatus run at the first send-out of every battle.
self:clearAllVolatiles() self:clearAllVolatiles()
@@ -1655,7 +1662,7 @@ function Battle:useMove(attacker, defender, moveId)
state.rolloutLock = nil state.rolloutLock = nil
end end
local hits = Effects.hitCount(def.effect, self.random) local hits = Effects.hitCount(def.effect, self:roller())
local landed = 0 local landed = 0
for hit = 1, hits do for hit = 1, hits do
if (defender.hp or 0) <= 0 then break end if (defender.hp or 0) <= 0 then break end
@@ -3178,7 +3185,8 @@ function Battle:giveExperiencePass(loser, def, recipients, count, halved)
-- ChangeHappiness is outside the level loop. -- ChangeHappiness is outside the level loop.
Happiness.change(mon, "GAINLEVEL") Happiness.change(mon, "GAINLEVEL")
self:emit({ kind = "level", index = index, level = mon.level, self:emit({ kind = "level", index = index, level = mon.level,
text = self:monName(mon) .. " grew to level " .. mon.level .. "!" }) text = self:monName(mon) .. " grew to level " .. mon.level .. "!",
sfx = "Sfx_DexFanfare5079", waitSfx = true })
for _, moveId in ipairs(result.learned) do for _, moveId in ipairs(result.learned) do
local ok, reason, entry = Mon.learnMove(mon, moveId, self.data) local ok, reason, entry = Mon.learnMove(mon, moveId, self.data)
local moveDef = self:moveDef(moveId) local moveDef = self:moveDef(moveId)
@@ -3644,7 +3652,7 @@ end
-- Running: Gen 2's odds (engine/battle/core.asm TryToRunAwayFromBattle) are -- Running: Gen 2's odds (engine/battle/core.asm TryToRunAwayFromBattle) are
-- based on the speed ratio and how many times you have tried this battle. -- based on the speed ratio and how many times you have tried this battle.
-- Trainers never let you run. -- Trainers never let you run.
function Battle:tryRun() function Battle:tryRun(pSpd)
-- .cant_escape and .cant_run_from_trainer leave wBattlePlayerAction alone, -- .cant_escape and .cant_run_from_trainer leave wBattlePlayerAction alone,
-- which is what BattleMenu_Run reads to decide whether the turn was spent -- which is what BattleMenu_Run reads to decide whether the turn was spent
-- (engine/battle/core.asm:5035); only .cant_escape_2, the failed roll at the -- (engine/battle/core.asm:5035); only .cant_escape_2, the failed roll at the
@@ -3676,7 +3684,8 @@ function Battle:tryRun()
return false return false
end end
self.runAttempts = (self.runAttempts or 0) + 1 self.runAttempts = (self.runAttempts or 0) + 1
if self:runRoll(self:effectiveSpeed(self.player), -- engine/battle/core.asm:2614
if self:runRoll(pSpd or self:effectiveSpeed(self.player),
self:effectiveSpeed(self.enemy)) then self:effectiveSpeed(self.enemy)) then
self:emit({ kind = "run", text = "Got away safely!" }) self:emit({ kind = "run", text = "Got away safely!" })
self:endBattle("run") self:endBattle("run")
+13 -6
View File
@@ -100,21 +100,28 @@ end
-- are 1/8 each, which is what the `and 3` on a 0-3 roll plus the two-step -- are 1/8 each, which is what the `and 3` on a 0-3 roll plus the two-step
-- fallthrough in .DetermineNumberOfHits produces. -- fallthrough in .DetermineNumberOfHits produces.
function Effects.multiHitCount(random) function Effects.multiHitCount(random)
local roll = random and random(4) or 0 local function roll(n)
if roll < 2 then return roll + 2 end if random then return random(n) end
-- Hits 4 and 5 take a second roll, so each ends up half as likely. if love and love.math and love.math.random then
local second = random and random(2) or 0 return love.math.random(n) - 1
return second + 4 end
return math.random(n) - 1
end
-- engine/battle/effect_commands.asm:5228
local first = roll(4)
if first < 2 then return first + 2 end
return roll(4) + 2
end end
Effects.HIT_COUNTS = { Effects.HIT_COUNTS = {
EFFECT_DOUBLE_HIT = 2, EFFECT_DOUBLE_HIT = 2,
EFFECT_POISON_MULTI_HIT = 2,
-- Triple Kick stops early if a hit misses; Battle rolls that per hit. -- Triple Kick stops early if a hit misses; Battle rolls that per hit.
EFFECT_TRIPLE_KICK = 3, EFFECT_TRIPLE_KICK = 3,
} }
function Effects.hitCount(effect, random) function Effects.hitCount(effect, random)
if effect == "EFFECT_MULTI_HIT" or effect == "EFFECT_POISON_MULTI_HIT" then if effect == "EFFECT_MULTI_HIT" then
return Effects.multiHitCount(random) return Effects.multiHitCount(random)
end end
return Effects.HIT_COUNTS[effect] or 1 return Effects.HIT_COUNTS[effect] or 1
+26 -3
View File
@@ -68,7 +68,16 @@ function Mon.stats(baseStats, dvs, level, statExp)
baseStats = baseStats or {} baseStats = baseStats or {}
dvs = dvs or {} dvs = dvs or {}
statExp = statExp or {} statExp = statExp or {}
local hpDv = dvs.hp or Mon.hpDV(dvs) -- engine/pokemon/move_mon.asm:1540
local specialDv = dvs.special
if specialDv == nil then
specialDv = dvs.specialAttack or dvs.specialDefense
end
-- engine/pokemon/move_mon.asm:1496
local hpDv = Mon.hpDV({
attack = dvs.attack, defense = dvs.defense,
speed = dvs.speed, special = specialDv,
})
local hp = math.floor((((baseStats.hp or 1) * 2 + hpDv * 2 local hp = math.floor((((baseStats.hp or 1) * 2 + hpDv * 2
+ math.floor(math.sqrt(statExp.hp or 0) / 4)) * level) / 100) + math.floor(math.sqrt(statExp.hp or 0) / 4)) * level) / 100)
+ level + 10 + level + 10
@@ -82,13 +91,27 @@ function Mon.stats(baseStats, dvs, level, statExp)
-- box_struct ends them at SpcExp), so SpA and SpD grow together. The -- box_struct ends them at SpcExp), so SpA and SpD grow together. The
-- per-stat keys are still read as a fallback for a record written before -- per-stat keys are still read as a fallback for a record written before
-- the shared word existed. -- the shared word existed.
specialAttack = statValue(baseStats.specialAttack, dvs.special, level, specialAttack = statValue(baseStats.specialAttack, specialDv, level,
statExp.special or statExp.specialAttack), statExp.special or statExp.specialAttack),
specialDefense = statValue(baseStats.specialDefense, dvs.special, level, specialDefense = statValue(baseStats.specialDefense, specialDv, level,
statExp.special or statExp.specialDefense), statExp.special or statExp.specialDefense),
} }
end end
function Mon.refreshStats(mon, data)
if type(mon) ~= "table" then return mon end
local def = data and data.pokemon and data.pokemon[mon.species]
if not (def and def.baseStats) then return mon end
-- engine/pokemon/move_mon.asm:1402
local stats = Mon.stats(def.baseStats, mon.dvs, mon.level or 1, mon.statExp)
mon.stats = stats
mon.maxHp = stats.hp
if mon.hp == nil or mon.hp > stats.hp then
mon.hp = stats.hp
end
return mon
end
-- The five stat exp words, in struct order. There is no sixth: see Mon.stats. -- The five stat exp words, in struct order. There is no sixth: see Mon.stats.
Mon.STAT_EXP_ORDER = { "hp", "attack", "defense", "speed", "special" } Mon.STAT_EXP_ORDER = { "hp", "attack", "defense", "speed", "special" }
+4 -5
View File
@@ -1906,11 +1906,10 @@ function Game2:applyOptions()
require("src.render.Pipelines").applyOptions(options) require("src.render.Pipelines").applyOptions(options)
-- src/core/Game.lua:1121 mirrors this call for Gen 1 -- src/core/Game.lua:1121 mirrors this call for Gen 1
Input:applyBindings(options.bindings) Input:applyBindings(options.bindings)
-- options.touchControls (the launcher editor's per-orientation layouts) and TouchControls:applyOptions({
-- options.haptics, the same two keys Gen 1 hands over here touchControls = options.touchControls,
-- (src/core/Game.lua:1073). One options.lua serves both games, so the pad a haptics = options.haptics,
-- player laid out for Red is already the pad Gold draws. })
TouchControls:applyOptions(options)
local GBCFX = require("src.render.GBCFX") local GBCFX = require("src.render.GBCFX")
if GBCFX.applyOptions(options) and self.save then if GBCFX.applyOptions(options) and self.save then
-- applyOptions returns true when it had to clear an unsupported level. -- applyOptions returns true when it had to clear an unsupported level.
+34
View File
@@ -119,6 +119,34 @@ function Input:reset()
self.stickAxis = { x = 0, y = 0 } self.stickAxis = { x = 0, y = 0 }
self.stickDir = nil self.stickDir = nil
self.hatDirs = {} self.hatDirs = {}
self.captureArmed = false
self.captureEvents = nil
end
function Input:armCapture()
self.captureArmed = true
self.captureEvents = {}
end
function Input:disarmCapture()
self.captureArmed = false
self.captureEvents = nil
end
function Input:takeCaptureEvents()
local ev = self.captureEvents
self.captureEvents = self.captureArmed and {} or nil
return ev
end
local function noteCapture(self, kind, phase, value)
if not self.captureArmed then return end
local ev = self.captureEvents
if not ev then
ev = {}
self.captureEvents = ev
end
ev[#ev + 1] = { kind = kind, phase = phase, value = value }
end end
-- Multiple physical sources (W + Up, d-pad + stick, etc.) can claim the -- Multiple physical sources (W + Up, d-pad + stick, etc.) can claim the
@@ -154,6 +182,7 @@ local function release(self, btn, source)
end end
function Input:keypressed(key) function Input:keypressed(key)
noteCapture(self, "key", "pressed", key)
local btn = self.keyBindings[key] local btn = self.keyBindings[key]
if btn then if btn then
press(self, btn, "key:" .. key) press(self, btn, "key:" .. key)
@@ -161,6 +190,7 @@ function Input:keypressed(key)
end end
function Input:keyreleased(key) function Input:keyreleased(key)
noteCapture(self, "key", "released", key)
local btn = self.keyBindings[key] local btn = self.keyBindings[key]
if btn then if btn then
release(self, btn, "key:" .. key) release(self, btn, "key:" .. key)
@@ -221,6 +251,7 @@ function Input:sourceRelease(btn, source)
end end
function Input:gamepadpressed(joystick, button) function Input:gamepadpressed(joystick, button)
noteCapture(self, "pad", "pressed", button)
local btn = self.padBindings[button] local btn = self.padBindings[button]
if btn then if btn then
press(self, btn, "pad:" .. button) press(self, btn, "pad:" .. button)
@@ -228,6 +259,7 @@ function Input:gamepadpressed(joystick, button)
end end
function Input:gamepadreleased(joystick, button) function Input:gamepadreleased(joystick, button)
noteCapture(self, "pad", "released", button)
local btn = self.padBindings[button] local btn = self.padBindings[button]
if btn then if btn then
release(self, btn, "pad:" .. button) release(self, btn, "pad:" .. button)
@@ -249,12 +281,14 @@ end
function Input:joystickpressed(joystick, button) function Input:joystickpressed(joystick, button)
if GamepadMap.ignoreRawForJoystick(joystick) then return end if GamepadMap.ignoreRawForJoystick(joystick) then return end
noteCapture(self, "joy", "pressed", button)
local btn = self.joyBindings[button] local btn = self.joyBindings[button]
if btn then press(self, btn, "joy:" .. button) end if btn then press(self, btn, "joy:" .. button) end
end end
function Input:joystickreleased(joystick, button) function Input:joystickreleased(joystick, button)
if GamepadMap.ignoreRawForJoystick(joystick) then return end if GamepadMap.ignoreRawForJoystick(joystick) then return end
noteCapture(self, "joy", "released", button)
local btn = self.joyBindings[button] local btn = self.joyBindings[button]
if btn then release(self, btn, "joy:" .. button) end if btn then release(self, btn, "joy:" .. button) end
end end
+4
View File
@@ -211,6 +211,10 @@ function TouchControls.defaultLayout(ww, wh, ox, oy, scale)
local abW = dpadW * 0.46 local abW = dpadW * 0.46
local ssW = dpadW * 0.30 local ssW = dpadW * 0.30
local margin = dpadW * 0.12 local margin = dpadW * 0.12
local ok, GameVersion = pcall(require, "src.core.GameVersion")
if ok and GameVersion.isGold and GameVersion.isGold() then
margin = math.max(margin, math.min(ww * 0.10, 72))
end
return { return {
dpad = { cx = ox + margin + dpadW / 2, cy = oy + wh - margin - dpadW / 2, w = dpadW }, dpad = { cx = ox + margin + dpadW / 2, cy = oy + wh - margin - dpadW / 2, w = dpadW },
a = { cx = ox + ww - margin - abW * 0.55, cy = oy + wh - margin - abW * 1.75, w = abW }, a = { cx = ox + ww - margin - abW * 0.55, cy = oy + wh - margin - abW * 1.75, w = abW },
+9
View File
@@ -275,6 +275,8 @@ Save.DEFAULT_OPTIONS = {
musicVol = 7, -- 0-7, like the GB's NR50 master volume musicVol = 7, -- 0-7, like the GB's NR50 master volume
sfxVol = 7, -- 0-7 sfxVol = 7, -- 0-7
musicFilter = 0, -- low-pass steps, 0 = off musicFilter = 0, -- low-pass steps, 0 = off
haptics = "light",
touchControls = { enabled = true },
} }
function Save.defaultOptions() function Save.defaultOptions()
@@ -719,6 +721,13 @@ end
-- copy is the witness that survives a crash mid-replace. -- copy is the witness that survives a crash mid-replace.
function Save.save(save) function Save.save(save)
if type(save) ~= "table" then return false, "no save" end if type(save) ~= "table" then return false, "no save" end
if (save.version or "gold") == "gold" then
local ok, SaveData = pcall(require, "src.core.SaveData")
if ok and SaveData.activeSlot and not SaveData.activeSlot("gold") then
local id = SaveData.createSlot and SaveData.createSlot("gold")
if id and SaveData.setActiveSlot then SaveData.setActiveSlot("gold", id) end
end
end
local main, backup, tmp = saveNames(save.version) local main, backup, tmp = saveNames(save.version)
local f = fs() local f = fs()
if not f then return false, "no filesystem" end if not f then return false, "no filesystem" end
+41 -2
View File
@@ -1370,6 +1370,10 @@ function RomImporter.new(onComplete, opts)
end end
end end
if GameVersion.VERSIONS[self.tab] then
self.modScope = self.tab
end
return self return self
end end
@@ -2627,6 +2631,9 @@ function RomImporter:_switchTab(id)
self.tab = id self.tab = id
self._findSearchFocus = false self._findSearchFocus = false
self:_disarmTextInput() self:_disarmTextInput()
if GameVersion.VERSIONS[id] then
self:_setModScope(id)
end
end end
function RomImporter:_toggleFindSearchFocus() function RomImporter:_toggleFindSearchFocus()
@@ -2937,6 +2944,13 @@ function RomImporter:_refreshMods()
end end
end end
self.mods = LauncherMods.list(self.modScope) or {} self.mods = LauncherMods.list(self.modScope) or {}
if self.modScope then
local kept = {}
for _, m in ipairs(self.mods) do
if m.targetsHere ~= false then kept[#kept + 1] = m end
end
self.mods = kept
end
self:_syncModUpdateInfo(false) self:_syncModUpdateInfo(false)
end end
@@ -3586,7 +3600,7 @@ function RomImporter:_findRows()
-- index, query, or category actually changed. -- index, query, or category actually changed.
local c = self._findRowsCache local c = self._findRowsCache
if c and c.src == all and c.query == self.findQuery if c and c.src == all and c.query == self.findQuery
and c.category == self.findCategory then and c.category == self.findCategory and c.scope == self.modScope then
return c.rows return c.rows
end end
local ModIndex = require("src.mods.ModIndex") local ModIndex = require("src.mods.ModIndex")
@@ -3594,8 +3608,33 @@ function RomImporter:_findRows()
query = self.findQuery, query = self.findQuery,
category = self.findCategory, category = self.findCategory,
}) })
if self.modScope then
local gen = GameVersion.generation(self.modScope)
local kept = {}
for _, entry in ipairs(rows) do
local has1, has2 = false, false
local function note(s)
s = tostring(s or ""):lower()
if s == "gen1" or s == "gen 1" or s == "red" or s == "blue"
or s == "yellow" then
has1 = true
end
if s == "gen2" or s == "gen 2" or s == "gold" then
has2 = true
end
end
for _, cat in ipairs(entry.categories or {}) do note(cat) end
for _, tag in ipairs(entry.tags or {}) do note(tag) end
if (not has1 and not has2)
or (gen == 2 and has2)
or (gen ~= 2 and has1) then
kept[#kept + 1] = entry
end
end
rows = kept
end
self._findRowsCache = { src = all, query = self.findQuery, self._findRowsCache = { src = all, query = self.findQuery,
category = self.findCategory, rows = rows } category = self.findCategory, scope = self.modScope, rows = rows }
return rows return rows
end end
+7 -1
View File
@@ -456,7 +456,13 @@ end
-- AskRockSmashScript reads it with `ifequal 1, .no`. Transcribing it the -- AskRockSmashScript reads it with `ifequal 1, .no`. Transcribing it the
-- obvious way round refuses the move for every party that has it. -- obvious way round refuses the move for every party that has it.
function H.HasRockSmash(ctx) function H.HasRockSmash(ctx)
return call(ctx, "partyMoveUser", "ROCK_SMASH") and 0 or 1 local mon = call(ctx, "partyMoveUser", "ROCK_SMASH")
if mon then
-- engine/events/overworld.asm:1339
ctx.curPartyMon = mon
return 0
end
return 1
end end
-- PutTheRodAway: ClearBox over the text window, then wPlayerAction back to -- PutTheRodAway: ClearBox over the text window, then wPlayerAction back to
+41 -4
View File
@@ -127,6 +127,7 @@ function BindingsMenu:beginCapture(item)
self.onKeyReleased = BindingsMenu.captureKeyRelease self.onKeyReleased = BindingsMenu.captureKeyRelease
self.onGamepadReleased = BindingsMenu.capturePadRelease self.onGamepadReleased = BindingsMenu.capturePadRelease
self.onJoystickReleased = BindingsMenu.captureJoyRelease self.onJoystickReleased = BindingsMenu.captureJoyRelease
if Input.armCapture then Input:armCapture() end
end end
function BindingsMenu:endCapture() function BindingsMenu:endCapture()
@@ -138,6 +139,7 @@ function BindingsMenu:endCapture()
self.onKeyReleased = nil self.onKeyReleased = nil
self.onGamepadReleased = nil self.onGamepadReleased = nil
self.onJoystickReleased = nil self.onJoystickReleased = nil
if Input.disarmCapture then Input:disarmCapture() end
end end
-- Escape is the capture's way out, so it is never captured: every other -- Escape is the capture's way out, so it is never captured: every other
@@ -232,7 +234,11 @@ function BindingsMenu:storeBinding(slot, value)
b[slot] = value b[slot] = value
opts.bindings[item.button.id] = b opts.bindings[item.button.id] = b
item.right = boundRight(opts.bindings, item.button) item.right = boundRight(opts.bindings, item.button)
if game.writeOptions then game:writeOptions() end if game.writeOptions then
game:writeOptions()
elseif game.persistOptions then
game:persistOptions()
end
end end
-- SELECT: forget one row's rebind and fall back to the defaults. #510's -- SELECT: forget one row's rebind and fall back to the defaults. #510's
@@ -246,7 +252,11 @@ function BindingsMenu:clearBinding(item)
end end
opts.bindings[item.button.id] = nil opts.bindings[item.button.id] = nil
item.right = boundRight(opts.bindings, item.button) item.right = boundRight(opts.bindings, item.button)
if game.writeOptions then game:writeOptions() end if game.writeOptions then
game:writeOptions()
elseif game.persistOptions then
game:persistOptions()
end
end end
-- START: confirm, then drop the whole overlay (#589). The footer doubles -- START: confirm, then drop the whole overlay (#589). The footer doubles
@@ -264,12 +274,39 @@ function BindingsMenu:confirmReset()
for _, it in ipairs(self.items) do for _, it in ipairs(self.items) do
it.right = boundRight(nil, it.button) it.right = boundRight(nil, it.button)
end end
if game.writeOptions then game:writeOptions() end if game.writeOptions then
game:writeOptions()
elseif game.persistOptions then
game:persistOptions()
end
end, { defaultNo = true })) end, { defaultNo = true }))
end end
function BindingsMenu:drainCapture()
local events = Input.takeCaptureEvents and Input:takeCaptureEvents()
if not events then return end
for i = 1, #events do
local ev = events[i]
if ev.phase == "pressed" then
if ev.kind == "key" then self:captureKey(ev.value)
elseif ev.kind == "pad" then self:capturePad(ev.value)
elseif ev.kind == "joy" then self:captureJoy(ev.value)
end
else
if ev.kind == "key" then self:captureKeyRelease(ev.value)
elseif ev.kind == "pad" then self:capturePadRelease(ev.value)
elseif ev.kind == "joy" then self:captureJoyRelease(ev.value)
end
end
if not self.capture then return end
end
end
function BindingsMenu:update(dt) function BindingsMenu:update(dt)
if self.capture then return end -- the raw capture owns the input if self.capture then
self:drainCapture()
return
end
if self.game.input:wasPressed("start") then if self.game.input:wasPressed("start") then
return self:confirmReset() return self:confirmReset()
end end
+94 -7
View File
@@ -49,17 +49,29 @@ end
function Editor.load(opts) function Editor.load(opts)
opts = opts or {} opts = opts or {}
Editor.onClose = opts.onClose Editor.onClose = opts.onClose
Editor.version = opts.version
Editor.hostPoll = opts.hostPoll == true
Editor.drag = nil Editor.drag = nil
Editor.rects = {} Editor.rects = {}
Editor._hostMouse = false
Editor._hostTouches = nil
Editor.fonts = { Editor.fonts = {
title = love.graphics.newFont(28), title = love.graphics.newFont(28),
body = love.graphics.newFont(16), body = love.graphics.newFont(16),
btn = love.graphics.newFont(18), btn = love.graphics.newFont(18),
} }
local optsTbl = SaveData.loadOptions() local optsTbl = SaveData.loadOptions()
local applied = optsTbl
if opts.version == "gold" then
local gold = type(optsTbl.gold) == "table" and optsTbl.gold or {}
applied = {
touchControls = gold.touchControls,
haptics = gold.haptics or optsTbl.haptics,
}
end
TouchControls:init() TouchControls:init()
TouchControls:ensureImages() TouchControls:ensureImages()
TouchControls:applyOptions(optsTbl) TouchControls:applyOptions(applied)
TouchControls:setPreview(true) TouchControls:setPreview(true)
Editor.enabled = TouchControls.enabled ~= false Editor.enabled = TouchControls.enabled ~= false
PadCursor.reset() PadCursor.reset()
@@ -71,17 +83,24 @@ function Editor.unload()
PadCursor.reset() PadCursor.reset()
Editor.drag = nil Editor.drag = nil
Editor.onClose = nil Editor.onClose = nil
Editor.version = nil
Editor._hostMouse = false
Editor._hostTouches = nil
end end
local function persist() local function persist()
local opts = SaveData.loadOptions() local opts = SaveData.loadOptions()
local cfg = TouchControls:config() local cfg = TouchControls:config()
-- replaces the whole table, so a pre-#633 top-level positions key is local block = {
-- dropped once the player saves under the new shape
opts.touchControls = {
enabled = cfg.enabled, enabled = cfg.enabled,
layouts = cfg.layouts, layouts = cfg.layouts,
} }
if Editor.version == "gold" then
opts.gold = type(opts.gold) == "table" and opts.gold or {}
opts.gold.touchControls = block
else
opts.touchControls = block
end
SaveData.saveOptions(opts) SaveData.saveOptions(opts)
end end
@@ -104,9 +123,7 @@ end
function Editor.update(dt) function Editor.update(dt)
PadCursor.update(dt or 0) PadCursor.update(dt or 0)
-- drag follows the live pointer when love.touch / mouse / pad is available; if Editor.hostPoll then Editor.pollHostPointers() end
-- touchmoved / mousemoved also update, so this is a belt-and-suspenders
-- path for Android where move events can be thin
if not Editor.drag then return end if not Editor.drag then return end
local x, y local x, y
if Editor.drag.touchId == "pad" then if Editor.drag.touchId == "pad" then
@@ -297,6 +314,45 @@ local function endDrag(id)
Editor.drag = nil Editor.drag = nil
end end
function Editor.pollHostPointers()
local down = love.mouse and love.mouse.isDown and love.mouse.isDown(1)
if down then
local x, y = love.mouse.getPosition()
if not Editor._hostMouse then
Editor._hostMouse = true
beginDrag("mouse", x, y)
else
moveDrag("mouse", x, y)
end
elseif Editor._hostMouse then
Editor._hostMouse = false
endDrag("mouse")
end
if not (love.touch and love.touch.getTouches and love.touch.getPosition) then
return
end
Editor._hostTouches = Editor._hostTouches or {}
local seen = {}
for _, id in ipairs(love.touch.getTouches()) do
seen[id] = true
local ok, tx, ty = pcall(love.touch.getPosition, id)
if ok and tx then
if not Editor._hostTouches[id] then
Editor._hostTouches[id] = true
beginDrag(id, tx, ty)
else
moveDrag(id, tx, ty)
end
end
end
for id in pairs(Editor._hostTouches) do
if not seen[id] then
Editor._hostTouches[id] = nil
endDrag(id)
end
end
end
function Editor.mousepressed(x, y, button) function Editor.mousepressed(x, y, button)
if button ~= 1 then return end if button ~= 1 then return end
-- Finger / mouse tap yields the Joy-Con pointer so the click lands where -- Finger / mouse tap yields the Joy-Con pointer so the click lands where
@@ -391,4 +447,35 @@ function Editor.keypressed(key)
end end
end end
function Editor.new(game)
local state = { game = game, isOpaque = true }
Editor.hostPoll = true
Editor.load({
version = "gold",
hostPoll = true,
onClose = function()
Editor.hostPoll = false
if game.options then
game.options.touchControls = TouchControls:config()
end
if game.stack and game.stack:top() == state then
game.stack:pop()
end
if game.applyOptions then game:applyOptions() end
end,
})
function state:wantsFillScale() return true end
function state:drawsWidescreen() return true end
function state:update(dt)
Editor.update(dt)
local input = self.game and self.game.input
if input and input:wasPressed("b") then close() end
end
function state:draw() end
function state:drawWidescreen(_w, _h)
Editor.draw()
end
return state
end
return Editor return Editor
+79 -5
View File
@@ -92,6 +92,8 @@ local FAINT_SLIDE_FRAMES_PER_ROW = 2
-- (engine/battle/core.asm:3439-3466, data/text/battle.asm:241-249). -- (engine/battle/core.asm:3439-3466, data/text/battle.asm:241-249).
local TEXT_NO_WILL_TO_FIGHT = "There's no will to battle!" local TEXT_NO_WILL_TO_FIGHT = "There's no will to battle!"
local TEXT_EGG_CANT_BATTLE = "An EGG can't battle!" local TEXT_EGG_CANT_BATTLE = "An EGG can't battle!"
-- data/text/battle.asm:207
local TEXT_USE_NEXT_MON = "Use next POKéMON?"
-- BattleText_TheMoveIsDisabled / BattleText_TheresNoPPLeftForThisMove -- BattleText_TheMoveIsDisabled / BattleText_TheresNoPPLeftForThisMove
-- (data/text/battle.asm:315-322). -- (data/text/battle.asm:315-322).
@@ -1146,11 +1148,11 @@ function BattleState:advanceQueue()
-- pokegold engine/battle/core.asm:7057-7069: every mon that leveled -- pokegold engine/battle/core.asm:7057-7069: every mon that leveled
-- gets the stats box, not just the mon currently on the field. -- gets the stats box, not just the mon currently on the field.
self.pendingStatsMon = mon self.pendingStatsMon = mon
-- BattleText_StringBuffer1GrewToLevel ends in text_end (battle.asm:336-343), -- engine/battle/core.asm:7044
-- and the active mon never even prints it (core.asm:7044-7056 jumps to the
-- stats box). Either way there is no PromptButton before the stats box.
if mon and mon == battle.player then if mon and mon == battle.player then
event.text = nil event.text = nil
event.sfx = nil
event.waitSfx = nil
if self.shownHp then if self.shownHp then
self.shownHp.player = mon.hp or 0 self.shownHp.player = mon.hp or 0
if self.hpAnim and self.hpAnim.side == "player" then if self.hpAnim and self.hpAnim.side == "player" then
@@ -1270,6 +1272,14 @@ function BattleState:advanceQueue()
return self:askNickname(event.mon) return self:askNickname(event.mon)
end end
if event.kind == "choose-switch" then if event.kind == "choose-switch" then
-- engine/battle/core.asm:2590
if self.battle and self.battle.wild then
self.nextMonIndex = 1
self.phase = "ask-next-mon"
self.message = TEXT_USE_NEXT_MON
self.messageTimer = 0
return
end
-- A fainted lead: force a switch before anything else runs. -- A fainted lead: force a switch before anything else runs.
self.phase = "forced-switch" self.phase = "forced-switch"
self.message = "Choose a POKéMON." self.message = "Choose a POKéMON."
@@ -1876,6 +1886,36 @@ function BattleState:update(_dt)
return return
end end
-- engine/battle/core.asm:2590
if self.phase == "ask-next-mon" then
if self.messageTimer > 0 then
if input:wasPressed("a") or input:wasPressed("b") then
self.messageTimer = 0
end
return
end
if input:wasPressed("up") or input:wasPressed("down") then
self.nextMonIndex = self.nextMonIndex == 1 and 2 or 1
elseif input:wasPressed("b") then
return self:answerUseNextMon(false)
elseif input:wasPressed("a") then
return self:answerUseNextMon(self.nextMonIndex == 1)
end
return
end
if self.phase == "cant-escape-then-switch" then
if self.messageTimer > 0 then
if input:wasPressed("a") or input:wasPressed("b") then
self.messageTimer = 0
end
return
end
self.message = "Choose a POKéMON."
self.phase = "forced-switch"
return
end
if self.phase == "refuse-shift" then if self.phase == "refuse-shift" then
if self.messageTimer > 0 then if self.messageTimer > 0 then
if input:wasPressed("a") or input:wasPressed("b") then if input:wasPressed("a") or input:wasPressed("b") then
@@ -2433,6 +2473,31 @@ function BattleState:offerShiftSwitch(mon)
self.messageTimer = MESSAGE_FRAMES self.messageTimer = MESSAGE_FRAMES
end end
function BattleState:answerUseNextMon(yes)
if yes then
self.phase = "forced-switch"
self.message = "Choose a POKéMON."
return
end
local battle = self.battle
if not battle then
self.phase = "forced-switch"
return
end
local lead = battle.party and battle.party[1]
local pSpd = (lead and lead.stats and lead.stats.speed) or 0
-- engine/battle/core.asm:2614
if battle:tryRun(pSpd) then
self:pushAll(battle:takeEvents())
self.phase = "resolving"
return self:advanceQueue()
end
battle:takeEvents()
self.message = "Can't escape!"
self.messageTimer = MESSAGE_FRAMES
self.phase = "cant-escape-then-switch"
end
-- SetUpBattlePartyMenu + PickSwitchMonInBattle (core.asm:3307-3308), which is -- SetUpBattlePartyMenu + PickSwitchMonInBattle (core.asm:3307-3308), which is
-- PARTYMENUACTION_SWITCH and carries no submenu; a cancel is `.canceled_switch` -- PARTYMENUACTION_SWITCH and carries no submenu; a cancel is `.canceled_switch`
-- and answers exactly like NO (:3327). -- and answers exactly like NO (:3327).
@@ -3173,15 +3238,18 @@ function BattleState:drawPanel()
-- stands. -- stands.
local asking = self.phase == "ask-nickname" or self.phase == "ask-forget" local asking = self.phase == "ask-nickname" or self.phase == "ask-forget"
or self.phase == "stop-learning" or self.phase == "ask-shift" or self.phase == "stop-learning" or self.phase == "ask-shift"
or self.phase == "ask-next-mon"
if asking and (self.messageTimer or 0) <= 0 then if asking and (self.messageTimer or 0) <= 0 then
-- OfferSwitch calls PlaceYesNoBox with `lb bc, 1, 7`, so its box is at -- OfferSwitch calls PlaceYesNoBox with `lb bc, 1, 7`, so its box is at
-- (1,7) instead (engine/battle/core.asm:3303, home/menu.asm:392-410). -- (1,7) instead (engine/battle/core.asm:3303, home/menu.asm:392-410).
local left = self.phase == "ask-shift" and 1 or 14 local left = (self.phase == "ask-shift" or self.phase == "ask-next-mon")
and 1 or 14
Chrome.box(left, 7, 6, 5) Chrome.box(left, 7, 6, 5)
Chrome.print("YES", left + 2, 8) Chrome.print("YES", left + 2, 8)
Chrome.print("NO", left + 2, 10) Chrome.print("NO", left + 2, 10)
local index = self.phase == "ask-nickname" and self.nicknameIndex local index = self.phase == "ask-nickname" and self.nicknameIndex
or self.phase == "ask-shift" and self.shiftIndex or self.phase == "ask-shift" and self.shiftIndex
or self.phase == "ask-next-mon" and self.nextMonIndex
or self.forgetChoice or self.forgetChoice
Chrome.cursor(left + 1, index == 1 and 8 or 10) Chrome.cursor(left + 1, index == 1 and 8 or 10)
end end
@@ -3201,7 +3269,13 @@ local STATS_BOX_ROWS = {
-- pokegold engine/battle/core.asm:7060-7066 (box at hlcoord 9,0, stats at 11,y). -- pokegold engine/battle/core.asm:7060-7066 (box at hlcoord 9,0, stats at 11,y).
function BattleState:drawStatsBox(mon) function BattleState:drawStatsBox(mon)
local stats = mon and mon.stats if not mon then return end
local stats = mon.stats
local data = self.game and self.game.data
local def = data and data.pokemon and data.pokemon[mon.species]
if def and def.baseStats then
stats = Mon.stats(def.baseStats, mon.dvs, mon.level, mon.statExp)
end
if not stats then return end if not stats then return end
Chrome.textbox(9, 0, 9, 10) Chrome.textbox(9, 0, 9, 10)
for i, row in ipairs(STATS_BOX_ROWS) do for i, row in ipairs(STATS_BOX_ROWS) do
+4 -22
View File
@@ -75,7 +75,8 @@ local SFX_TRANSACTION = "Sfx_Transaction"
local MartMenu = {} local MartMenu = {}
MartMenu.__index = MartMenu MartMenu.__index = MartMenu
MartMenu.isOpaque = true -- engine/items/mart.asm:54
MartMenu.isOpaque = false
-- constants/mart_constants.asm. The `pokemart` macro emits this as one byte -- constants/mart_constants.asm. The `pokemart` macro emits this as one byte
-- ahead of the word mart id, and MartTypeDialogs is indexed by it. -- ahead of the word mart id, and MartTypeDialogs is indexed by it.
@@ -330,9 +331,6 @@ local function extractedText(text, base, labels)
return out return out
end end
function MartMenu:wantsFillScale() return true end
function MartMenu:drawsWidescreen() return true end
-- data/generated/marts.lua is what the ROM extractor will write out of `Marts` -- data/generated/marts.lua is what the ROM extractor will write out of `Marts`
-- (data/items/marts.asm): `lists` is a 1-based array in MART_* order, each -- (data/items/marts.asm): `lists` is a 1-based array in MART_* order, each
-- entry an array of item ids, and `bargain` is BargainShopData's own -- entry an array of item ids, and `bargain` is BargainShopData's own
@@ -896,7 +894,8 @@ local function printPriceOpaque(amount, ty)
end end
function MartMenu:drawBuyList() function MartMenu:drawBuyList()
-- pokegold engine/menus/scrolling_menu.asm _InitScrollingMenu: no border for the buy list -- engine/items/mart.asm:542
Chrome.box(LIST_BOX_X, LIST_BOX_Y, LIST_BOX_W, LIST_BOX_H)
for row = 1, VISIBLE_ROWS do for row = 1, VISIBLE_ROWS do
local i = row + self.scroll local i = row + self.scroll
local ty = LIST_Y + (row - 1) * LIST_SPACING local ty = LIST_Y + (row - 1) * LIST_SPACING
@@ -953,18 +952,14 @@ end
function MartMenu:drawUnder() function MartMenu:drawUnder()
local phase = self.phase local phase = self.phase
if phase == "top" then if phase == "top" then
Chrome.clear()
self:drawTopMenu() self:drawTopMenu()
self:drawTextBox(self.topLines) self:drawTextBox(self.topLines)
elseif phase == "buy" or phase == "buyQuantity" then elseif phase == "buy" or phase == "buyQuantity" then
Chrome.clear()
self:drawMoneyBox() self:drawMoneyBox()
self:drawBuyList() self:drawBuyList()
self:drawDescription() self:drawDescription()
elseif phase == "sell" or phase == "sellQuantity" then elseif phase == "sell" or phase == "sellQuantity" then
if self.pack then self.pack:drawPanel() end if self.pack then self.pack:drawPanel() end
else
Chrome.clear()
end end
end end
@@ -1003,17 +998,4 @@ function MartMenu:draw()
self:drawPanel() self:drawPanel()
end end
function MartMenu:drawWidescreen(winW, winH)
local G = love.graphics
G.setColor(1, 1, 1, 1)
G.rectangle("fill", 0, 0, winW, winH)
local scale = Chrome.fitScale(winW, winH)
G.push()
G.translate(math.floor((winW - 160 * scale) / 2),
math.floor((winH - 144 * scale) / 2))
G.scale(scale, scale)
self:drawPanel()
G.pop()
end
return MartMenu return MartMenu
+55 -21
View File
@@ -119,7 +119,13 @@ function NamingScreen.new(game, opts)
-- The header icon is an OBJ on the cart, so it wears a real palette; without -- The header icon is an OBJ on the cart, so it wears a real palette; without
-- one it would draw in raw DMG shades next to a colored world. -- one it would draw in raw DMG shades next to a colored world.
self.iconColors = opts.iconColors self.iconColors = opts.iconColors
self.gfx = opts.menuGfx local data = game and game.data or {}
self.gfx = opts.menuGfx or data.gen2MenuGfx
if self.gfx and self.gfx.naming then self.gfx = self.gfx.naming end
-- engine/menus/naming_screen.asm:47
-- engine/gfx/cgb_layouts.asm:488
local diploma = data.gen2Diploma
self.palette = diploma and diploma.palettes and diploma.palettes[1]
self.tiles = {} self.tiles = {}
if self.gfx then if self.gfx then
for _, key in ipairs({ "border", "middleLine", "underLine", "cursor" }) do for _, key in ipairs({ "border", "middleLine", "underLine", "cursor" }) do
@@ -315,6 +321,10 @@ function NamingScreen:update(_dt)
end end
end end
function NamingScreen:paperColor()
return GbcPalette.color(self.palette, 1)
end
-- The backdrop: one patterned tile repeated over the whole screen. Without -- The backdrop: one patterned tile repeated over the whole screen. Without
-- menu_gfx.lua (older cache) fall back to a flat mid gray, which keeps the -- menu_gfx.lua (older cache) fall back to a flat mid gray, which keeps the
-- cleared panels readable. -- cleared panels readable.
@@ -322,17 +332,25 @@ function NamingScreen:drawBackdrop()
local G = love.graphics local G = love.graphics
local tile = self.tiles.border local tile = self.tiles.border
if not tile then if not tile then
G.setColor(0.62, 0.62, 0.62, 1) local paper = self:paperColor()
G.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
G.rectangle("fill", 0, 0, 160, 144) G.rectangle("fill", 0, 0, 160, 144)
G.setColor(1, 1, 1, 1) G.setColor(1, 1, 1, 1)
return return
end end
G.setColor(1, 1, 1, 1) local function blit()
for ty = 0, Chrome.SCREEN_H - 1 do G.setColor(1, 1, 1, 1)
for tx = 0, Chrome.SCREEN_W - 1 do for ty = 0, Chrome.SCREEN_H - 1 do
G.draw(tile, tx * 8, ty * 8) for tx = 0, Chrome.SCREEN_W - 1 do
G.draw(tile, tx * 8, ty * 8)
end
end end
end end
if self.palette and GbcPalette.available() then
GbcPalette.with(self.palette, blit)
else
blit()
end
end end
-- The cursor (data/sprite_anims/oam.asm .OAMData_TextEntryCursor and -- The cursor (data/sprite_anims/oam.asm .OAMData_TextEntryCursor and
@@ -410,7 +428,8 @@ end
function NamingScreen:clearPanel(tx, ty, tw, th) function NamingScreen:clearPanel(tx, ty, tw, th)
local G = love.graphics local G = love.graphics
G.setColor(1, 1, 1, 1) local paper = self:paperColor()
G.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
G.rectangle("fill", tx * 8, ty * 8, tw * 8, th * 8) G.rectangle("fill", tx * 8, ty * 8, tw * 8, th * 8)
G.setColor(0, 0, 0, 1) G.setColor(0, 0, 0, 1)
end end
@@ -472,12 +491,13 @@ function NamingScreen:drawPanel()
G.draw(self.iconImage, quad, 16, 16) G.draw(self.iconImage, quad, 16, 16)
end end
end end
local pal = self.palette
if self.monName then if self.monName then
-- Nickname header is two lines: "<MON>'S" then "NICKNAME?". -- Nickname header is two lines: "<MON>'S" then "NICKNAME?".
Chrome.print(self.monName .. "'S", 5, 2) Chrome.printThrough(self.monName .. "'S", 5, 2, pal)
Chrome.print("NICKNAME?", 5, 4) Chrome.printThrough("NICKNAME?", 5, 4, pal)
else else
Chrome.print(self.prompt, 5, 2) Chrome.printThrough(self.prompt, 5, 2, pal)
end end
self:drawEntry(5, self.isBox and 4 or 6) self:drawEntry(5, self.isBox and 4 or 6)
@@ -489,17 +509,22 @@ function NamingScreen:drawPanel()
for col = 0, 8 do for col = 0, 8 do
local ch = line[col + 1] local ch = line[col + 1]
if ch and ch ~= " " and ch ~= "" then if ch and ch ~= " " and ch ~= "" then
Chrome.print(ch, 2 + col * 2, keyboardTop + row * 2) Chrome.printThrough(ch, 2 + col * 2, keyboardTop + row * 2, pal)
end end
end end
end end
local labels = self.lower and BOTTOM_LOWER_LABELS or BOTTOM_UPPER_LABELS local labels = self.lower and BOTTOM_LOWER_LABELS or BOTTOM_UPPER_LABELS
local bottomY = keyboardTop + bottom * 2 local bottomY = keyboardTop + bottom * 2
for i, label in ipairs(labels) do for i, label in ipairs(labels) do
Chrome.print(label, BOTTOM_LABEL_TX[i], bottomY) Chrome.printThrough(label, BOTTOM_LABEL_TX[i], bottomY, pal)
end end
self:drawCursorBox(self:cursorTile()) local function cursor() self:drawCursorBox(self:cursorTile()) end
if pal and GbcPalette.available() then
GbcPalette.with(pal, cursor)
else
cursor()
end
G.setColor(1, 1, 1, 1) G.setColor(1, 1, 1, 1)
end end
@@ -511,23 +536,32 @@ function NamingScreen:drawWidescreen(winW, winH)
local G = love.graphics local G = love.graphics
-- The naming screen's own patterned backdrop is the surround: extend it to -- The naming screen's own patterned backdrop is the surround: extend it to
-- the window edges so a widescreen boot has no black pillarbox. -- the window edges so a widescreen boot has no black pillarbox.
self:drawBackdrop()
local scale = Chrome.fitScale(winW, winH) local scale = Chrome.fitScale(winW, winH)
local ox, oy = Chrome.fitOrigin(winW, winH, scale) local ox, oy = Chrome.fitOrigin(winW, winH, scale)
G.setColor(0.62, 0.62, 0.62, 1) local paper = self:paperColor()
G.setColor(paper[1] / 255, paper[2] / 255, paper[3] / 255, 1)
G.rectangle("fill", 0, 0, winW, winH) G.rectangle("fill", 0, 0, winW, winH)
G.setColor(1, 1, 1, 1) G.setColor(1, 1, 1, 1)
if self.tiles.border then if self.tiles.border then
local tilesX = math.ceil(winW / (8 * scale)) local tilesX = math.ceil(winW / (8 * scale))
local tilesY = math.ceil(winH / (8 * scale)) local tilesY = math.ceil(winH / (8 * scale))
G.push() local tile = self.tiles.border
G.scale(scale, scale) local function blit()
for ty = 0, tilesY do G.setColor(1, 1, 1, 1)
for tx = 0, tilesX do G.push()
G.draw(self.tiles.border, tx * 8, ty * 8) G.scale(scale, scale)
for ty = 0, tilesY do
for tx = 0, tilesX do
G.draw(tile, tx * 8, ty * 8)
end
end end
G.pop()
end
if self.palette and GbcPalette.available() then
GbcPalette.with(self.palette, blit)
else
blit()
end end
G.pop()
end end
G.push() G.push()
G.translate(ox, oy) G.translate(ox, oy)
+42 -4
View File
@@ -172,6 +172,34 @@ local ROWS = {
text = function(options) text = function(options)
return require("src.render.GBCFX").levelLabel(options.gbcfx or 0) return require("src.render.GBCFX").levelLabel(options.gbcfx or 0)
end }, end },
{ id = "touchControls", label = "TOUCH PAD", port = true,
text = function(options)
local tc = options.touchControls
local on = not (type(tc) == "table" and tc.enabled == false)
return on and "ON" or "OFF"
end,
cycle = function(options, _delta, game)
local tc = type(options.touchControls) == "table" and options.touchControls or {}
tc.enabled = tc.enabled == false
options.touchControls = tc
require("src.core.TouchControls"):applyOptions(options)
if game and game.persistOptions then game:persistOptions() end
end },
{ id = "touchLayout", label = "TOUCH LAYOUT", port = true,
activate = function(game)
game.stack:push(require("src.ui.TouchControlsEditor").new(game))
end },
{ id = "haptics", label = "VIBRATION", port = true,
text = function(options)
return require("src.core.TouchControls").hapticLabel(options.haptics)
end,
cycle = function(options, delta, game)
local TC = require("src.core.TouchControls")
options.haptics = TC.cycleHaptics(options.haptics, delta)
TC:applyOptions(options)
TC.buzz(options.haptics)
if game and game.persistOptions then game:persistOptions() end
end },
{ label = "CANCEL", cancel = true }, { label = "CANCEL", cancel = true },
} }
@@ -196,11 +224,18 @@ local function sameRows(_, rows) return rows end
-- COLOR) simply appear in the list the hook receives. -- COLOR) simply appear in the list the hook receives.
local function buildRows() local function buildRows()
local rows = {} local rows = {}
local env = os.getenv("POKEPORT_TOUCH")
local osName = love.system and love.system.getOS and love.system.getOS()
local showTouch = env == "1"
or (env ~= "0" and (osName == "Android" or osName == "iOS"))
for i, row in ipairs(ROWS) do for i, row in ipairs(ROWS) do
local copy = {} if showTouch or (row.id ~= "touchControls" and row.id ~= "touchLayout"
for key, value in pairs(row) do copy[key] = value end and row.id ~= "haptics") then
copy.id = copy.id or copy.key or (copy.cancel and "cancel") or nil local copy = {}
rows[i] = copy for key, value in pairs(row) do copy[key] = value end
copy.id = copy.id or copy.key or (copy.cancel and "cancel") or nil
rows[#rows + 1] = copy
end
end end
return rows return rows
end end
@@ -291,6 +326,9 @@ end
function OptionsMenu:leave_() function OptionsMenu:leave_()
if self.onDone then self.onDone(self.options) end if self.onDone then self.onDone(self.options) end
if self.game and self.game.stack and self.game.stack:top() == self then
self.game.stack:pop()
end
end end
function OptionsMenu:update(_dt) function OptionsMenu:update(_dt)
+66
View File
@@ -354,6 +354,12 @@ function PackMenu:useSelected()
end end
return return
end end
-- engine/items/tmhm.asm:73
local def = self.items and self.items[row.id]
if def and def.teaches then
self:openTeachParty(row)
return
end
-- UseItem's jumptable runs off ITEMATTR's field-menu nibble, and the first -- UseItem's jumptable runs off ITEMATTR's field-menu nibble, and the first
-- four entries are all .Oak -- an X ATTACK or a POKé DOLL used from the -- four entries are all .Oak -- an X ATTACK or a POKé DOLL used from the
-- field PACK prints OakThisIsntTheTimeText and goes nowhere. Only the -- field PACK prints OakThisIsntTheTimeText and goes nowhere. Only the
@@ -586,6 +592,66 @@ function PackMenu:giveToSlot(slot, row)
held:giveItem(row.id) held:giveItem(row.id)
end end
-- engine/items/tmhm.asm:73
function PackMenu:openTeachParty(row)
local game = self.game
local party = (self.save and self.save.party) or {}
if #party == 0 then
self.message = NO_POKEMON
return
end
if not (game and game.stack) then return end
if not pcall(Screens.get, game, "Gen2PartyMenu") then return end
local def = self.items and self.items[row.id]
local moveId = def and def.teaches
local moves = game.data and game.data.moves
local moveDef = moves and moves[moveId]
local moveName = (moveDef and moveDef.name) or moveId
self.staleRows = true
Screens.push(game, "Gen2PartyMenu", {
save = self.save,
prompt = "teach",
tmhm = { move = moveId },
onCancel = function()
game.stack:pop()
self:rebuild()
end,
onChoose = function(_slot, mon)
game.stack:pop()
local species = game.data and game.data.pokemon
and game.data.pokemon[mon.species]
local allowed = false
for _, id in ipairs((species and species.tmhm) or {}) do
if id == moveId then allowed = true end
end
if not allowed then
if game.say then
game:say(("%s can't learn %s!"):format(
mon.nickname or mon.species or "?", moveName))
end
return
end
for _, move in ipairs(mon.moves or {}) do
if move.id == moveId then
if game.say then
game:say(("%s already knows %s!"):format(
mon.nickname or mon.species or "?", moveName))
end
return
end
end
if not game.learnMoveOn then return end
game:learnMoveOn(mon, moveId, function(learned)
if not learned then return end
if tostring(row.id):sub(1, 3) == "HM_" then return end
require("src.core.gen2.Happiness").change(mon, "LEARNMOVE")
if game.consumeItem then game:consumeItem(row.id) end
self:rebuild()
end)
end,
})
end
function PackMenu:update(_dt) function PackMenu:update(_dt)
local input = self.game and self.game.input local input = self.game and self.game.input
if not input then return end if not input then return end
+37 -3
View File
@@ -22,6 +22,7 @@ local GbcPalette = require("src.render.GbcPalette")
local HpBar = require("src.battle.gen2.HpBar") local HpBar = require("src.battle.gen2.HpBar")
local Logger = require("src.core.Logger") local Logger = require("src.core.Logger")
local Mail = require("src.core.gen2.Mail") local Mail = require("src.core.gen2.Mail")
local Mon = require("src.battle.gen2.Mon")
local Runtime = require("src.mods.Runtime") local Runtime = require("src.mods.Runtime")
local Screens = require("src.ui.Screens") local Screens = require("src.ui.Screens")
@@ -90,11 +91,20 @@ function PartyMenu.new(game, opts)
self.save = save self.save = save
self.party = opts.party or (save and save.party) or {} self.party = opts.party or (save and save.party) or {}
local data = game and game.data or {} local data = game and game.data or {}
-- engine/pokemon/move_mon.asm:1402
for i = 1, #self.party do
Mon.refreshStats(self.party[i], data)
end
self.icons = opts.icons or data.gen2Icons self.icons = opts.icons or data.gen2Icons
self.palettes = opts.palettes or data.gen2Palettes self.palettes = opts.palettes or data.gen2Palettes
self.pokemon = opts.pokemon or data.pokemon self.pokemon = opts.pokemon or data.pokemon
self.prompt = PartyMenu.PROMPTS[opts.prompt or "choose"] or opts.prompt self.prompt = PartyMenu.PROMPTS[opts.prompt or "choose"] or opts.prompt
or PartyMenu.PROMPTS.choose or PartyMenu.PROMPTS.choose
-- engine/pokemon/party_menu.asm:297
self.tmhm = opts.tmhm
if self.tmhm and (opts.prompt == nil or opts.prompt == "teach") then
self.prompt = PartyMenu.PROMPTS.teach
end
self.onChoose = opts.onChoose self.onChoose = opts.onChoose
self.onCancel = opts.onCancel self.onCancel = opts.onCancel
self.moves = opts.moves or data.moves self.moves = opts.moves or data.moves
@@ -469,7 +479,14 @@ function PartyMenu:update(_dt)
elseif self.wantsSubmenu or self.wantsBattleSubmenu then elseif self.wantsSubmenu or self.wantsBattleSubmenu then
self:openSubmenu() self:openSubmenu()
elseif self.onChoose then elseif self.onChoose then
self.onChoose(self.index, self.party[self.index]) local mon = self.party[self.index]
if self.tmhm and mon and mon.isEgg then
-- engine/items/tmhm.asm:104
local world = self.game and self.game.world
if world and world.playSfxNamed then world:playSfxNamed("Sfx_Wrong") end
return
end
self.onChoose(self.index, mon)
end end
elseif input:wasPressed("b") then elseif input:wasPressed("b") then
self:storeCursor() self:storeCursor()
@@ -674,6 +691,18 @@ function PartyMenu.rowFor(mon)
} }
end end
-- engine/pokemon/party_menu.asm:331
function PartyMenu:tmhmAble(mon)
if not mon or mon.isEgg then return nil end
local move = self.tmhm and self.tmhm.move
if not move then return nil end
local species = self.pokemon and self.pokemon[mon.species]
for _, id in ipairs((species and species.tmhm) or {}) do
if id == move then return "ABLE" end
end
return "NOT ABLE"
end
-- WritePartyMenuTilemap, jumptable entry by jumptable entry. Every coordinate -- WritePartyMenuTilemap, jumptable entry by jumptable entry. Every coordinate
-- below is the hlcoord the matching PARTYMENUQUALITY_* routine uses, and each -- below is the hlcoord the matching PARTYMENUQUALITY_* routine uses, and each
-- steps 2 * SCREEN_WIDTH per mon: -- steps 2 * SCREEN_WIDTH per mon:
@@ -712,10 +741,15 @@ function PartyMenu:drawPanel()
self:drawIcon(mon, self:iconX(i), 4 + (i - 1) * 16 + self:iconBob(i)) self:drawIcon(mon, self:iconX(i), 4 + (i - 1) * 16 + self:iconBob(i))
local row = PartyMenu.rowFor(mon) local row = PartyMenu.rowFor(mon)
Chrome.print(row.name, 3, nameY) Chrome.print(row.name, 3, nameY)
if row.hp then Chrome.print(row.hp, 13, nameY) end if self.tmhm then
local able = self:tmhmAble(mon)
if able then Chrome.print(able, 12, dataY) end
else
if row.hp then Chrome.print(row.hp, 13, nameY) end
if row.hp then self:drawHpBar(mon, 11, dataY) end
end
if row.status then Chrome.print(row.status, 5, dataY) end if row.status then Chrome.print(row.status, 5, dataY) end
if row.level then Chrome.print(row.level, 8, dataY) end if row.level then Chrome.print(row.level, 8, dataY) end
if row.hp then self:drawHpBar(mon, 11, dataY) end
end end
-- .end does `dec hl` twice from the row past the last nickname, so CANCEL -- .end does `dec hl` twice from the row past the last nickname, so CANCEL
+27 -9
View File
@@ -1084,16 +1084,20 @@ function Pokegear:update(_dt)
-- answers -1 rather than backing out to the strip. -- answers -1 rather than backing out to the strip.
if self.fly then return self:updateFlyMap(input) end if self.fly then return self:updateFlyMap(input) end
if self.mode == "strip" then if self.mode == "strip" then
if input:wasPressed("left") then local stripCard = self:card()
self.cardIndex = self.cardIndex > 1 and self.cardIndex - 1 or #self.cards if not (stripCard and stripCard.id == "phone") then
elseif input:wasPressed("right") then if input:wasPressed("left") then
self.cardIndex = self.cardIndex < #self.cards and self.cardIndex + 1 or 1 self.cardIndex = self.cardIndex > 1 and self.cardIndex - 1 or #self.cards
elseif input:wasPressed("a") then elseif input:wasPressed("right") then
self.mode = "card" self.cardIndex = self.cardIndex < #self.cards and self.cardIndex + 1 or 1
elseif input:wasPressed("b") then elseif input:wasPressed("a") then
if self.onClose then self.onClose() end self.mode = "card"
elseif input:wasPressed("b") then
if self.onClose then self.onClose() end
end
return
end end
return self.mode = "card"
end end
-- Inside a card. -- Inside a card.
local card = self:card() local card = self:card()
@@ -1105,6 +1109,19 @@ function Pokegear:update(_dt)
-- submenu is waiting for. -- submenu is waiting for.
local phoneBusy = card and card.id == "phone" local phoneBusy = card and card.id == "phone"
and (self.call ~= nil or self.phoneSubmenu ~= nil) and (self.call ~= nil or self.phoneSubmenu ~= nil)
-- engine/pokegear/pokegear.asm:799
if card and card.id == "phone" and not phoneBusy then
if input:wasPressed("b") then
if self.onClose then self.onClose() end
return
elseif input:wasPressed("left") then
self:switchCard("map", "clock")
return
elseif input:wasPressed("right") then
self:switchCard("radio")
return
end
end
if not phoneBusy and input:wasPressed("b") then if not phoneBusy and input:wasPressed("b") then
self.mode = "strip" self.mode = "strip"
self:stopRadio() self:stopRadio()
@@ -2057,6 +2074,7 @@ function Pokegear:drawPlayerIcon(x, y)
local beat = math.floor((self.iconTimer or 0) / 8) local beat = math.floor((self.iconTimer or 0) / 8)
-- .OAMData_RedWalk (data/sprite_anims/oam.asm:314-319) hangs its four tiles -- .OAMData_RedWalk (data/sprite_anims/oam.asm:314-319) hangs its four tiles
-- at -8,-8; camY of -4 undoes the world's sprite lift. -- at -8,-8; camY of -4 undoes the world's sprite lift.
love.graphics.setColor(1, 1, 1, 1)
self.playerIcon:draw(x - 8, y - 8, 0, -4, "down", self.playerIcon:draw(x - 8, y - 8, 0, -4, "down",
beat % 2, beat == 3) beat % 2, beat == 3)
return true return true
+6
View File
@@ -261,6 +261,8 @@ function SummaryMenu.new(game, opts)
math.min(opts.index or 1, math.max(1, #self.party))) math.min(opts.index or 1, math.max(1, #self.party)))
self.mon = self.party[self.index] self.mon = self.party[self.index]
end end
-- engine/pokemon/move_mon.asm:1402
Mon.refreshStats(self.mon, data)
self.page = opts.page or PINK_PAGE self.page = opts.page or PINK_PAGE
-- ManagePokemonMoves opens straight onto MoveScreenLoop's screen; SELECT off -- ManagePokemonMoves opens straight onto MoveScreenLoop's screen; SELECT off
-- the green page reaches the same view with the stats pages still behind it. -- the green page reaches the same view with the stats pages still behind it.
@@ -678,6 +680,8 @@ function SummaryMenu:switchMon(delta)
if next_ < 1 or next_ > #self.party then return false end if next_ < 1 or next_ > #self.party then return false end
self.index = next_ self.index = next_
self.mon = self.party[next_] self.mon = self.party[next_]
-- engine/pokemon/move_mon.asm:1402
Mon.refreshStats(self.mon, self.game and self.game.data)
self.moveIndex = 1 self.moveIndex = 1
self:playCry() self:playCry()
return true return true
@@ -694,6 +698,8 @@ function SummaryMenu:switchMonPastEggs(delta)
if next_ < 1 or next_ > #self.party then return false end if next_ < 1 or next_ > #self.party then return false end
self.index = next_ self.index = next_
self.mon = self.party[next_] self.mon = self.party[next_]
-- engine/pokemon/move_mon.asm:1402
Mon.refreshStats(self.mon, self.game and self.game.data)
self.moveIndex = 1 self.moveIndex = 1
self:playCry() self:playCry()
return true return true
+30 -3
View File
@@ -45,6 +45,7 @@
local Anim = require("src.core.gen2.TradeAnim") local Anim = require("src.core.gen2.TradeAnim")
local Assets = require("src.render.Assets") local Assets = require("src.render.Assets")
local Chrome = require("src.ui.gen2.Chrome") local Chrome = require("src.ui.gen2.Chrome")
local Font = require("src.render.Font")
local GbcPalette = require("src.render.GbcPalette") local GbcPalette = require("src.render.GbcPalette")
local Music = require("src.core.Music") local Music = require("src.core.Music")
local Palettes = require("src.world.gen2.Palettes") local Palettes = require("src.world.gen2.Palettes")
@@ -202,6 +203,15 @@ local TEMPLATE_ROWS = {
{ row = 6, text = "<ID>№." }, { row = 6, text = "<ID>№." },
} }
-- gfx/sgb/predef.pal:29
local function scale5(value) return math.floor(value * 255 / 31 + 0.5) end
local TRADE_TUBE_PAL = {
{ scale5(31), scale5(31), scale5(31) },
{ scale5(18), scale5(20), scale5(27) },
{ scale5(11), scale5(15), scale5(23) },
{ 0, 0, 0 },
}
-------------------------------------------------------------------------- --------------------------------------------------------------------------
-- Construction -- Construction
-------------------------------------------------------------------------- --------------------------------------------------------------------------
@@ -367,8 +377,22 @@ end
-- reading through. -- reading through.
-- `index` is a GB shade, 0 (white) to 3 (black); GbcPalette.color counts from -- `index` is a GB shade, 0 (white) to 3 (black); GbcPalette.color counts from
-- 1, as the palettes themselves do. -- 1, as the palettes themselves do.
function TradeAnimView:bgColors()
local id = (self.beat or {}).id or ""
local pan = id:find("_pan_", 1, true)
if not pan then
return Palettes.textColors(self.palettes)
end
local colors = TRADE_TUBE_PAL
-- engine/movie/trade_animation.asm:1271
if math.floor((self.frame or 0) / 8) % 2 == 1 then
colors = { colors[1], colors[3], colors[2], colors[4] }
end
return colors
end
function TradeAnimView:shade(index) function TradeAnimView:shade(index)
local colors = Palettes.textColors(self.palettes) local colors = self:bgColors()
local rgb = GbcPalette.color(colors, index + 1) local rgb = GbcPalette.color(colors, index + 1)
or ({ { 255, 255, 255 }, { 168, 168, 168 }, { 96, 96, 96 }, or ({ { 255, 255, 255 }, { 168, 168, 168 }, { 96, 96, 96 },
{ 0, 0, 0 } })[index + 1] { 0, 0, 0 } })[index + 1]
@@ -384,7 +408,7 @@ end
-- and GbcPalette maps them. A driver with no shader draws the greys, which -- and GbcPalette maps them. A driver with no shader draws the greys, which
-- is the DMG ramp and not a black frame. -- is the DMG ramp and not a black frame.
function TradeAnimView:through(body) function TradeAnimView:through(body)
local colors = Palettes.textColors(self.palettes) local colors = self:bgColors()
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
if colors and GbcPalette.available() then if colors and GbcPalette.available() then
GbcPalette.with(colors, body) GbcPalette.with(colors, body)
@@ -543,7 +567,7 @@ function TradeAnimView:drawStats(record, offset)
G.setColor(1, 1, 1, 1) G.setColor(1, 1, 1, 1)
G.rectangle("fill", (PANEL_X + 1) * 8, PANEL_Y * 8, 9 * 8, 8) G.rectangle("fill", (PANEL_X + 1) * 8, PANEL_Y * 8, 9 * 8, 8)
for _, row in ipairs(TEMPLATE_ROWS) do for _, row in ipairs(TEMPLATE_ROWS) do
Chrome.print(row.text, PANEL_X + 1, row.row) Chrome.print(Strings(row.text), PANEL_X + 1, row.row)
end end
Chrome.print(Chrome.number(record.dex or 0, 3, true), PANEL_X + 7, 0) Chrome.print(Chrome.number(record.dex or 0, 3, true), PANEL_X + 7, 0)
Chrome.print(record.name, PANEL_X + 1, 2) Chrome.print(record.name, PANEL_X + 1, 2)
@@ -767,6 +791,8 @@ end
function TradeAnimView:drawPanel() function TradeAnimView:drawPanel()
local id = (self.beat or {}).id local id = (self.beat or {}).id
local t = self.offset or 0 local t = self.offset or 0
-- engine/movie/trade_animation.asm:151
local wasBattle = Font.useBattleExtra(true)
Chrome.clear() Chrome.clear()
if GIVE_BEATS[id] then if GIVE_BEATS[id] then
@@ -794,6 +820,7 @@ function TradeAnimView:drawPanel()
end end
end end
love.graphics.setColor(1, 1, 1, 1) love.graphics.setColor(1, 1, 1, 1)
Font.useBattleExtra(wasBattle)
end end
function TradeAnimView:drawTubeBeat(id, t) function TradeAnimView:drawTubeBeat(id, t)
+34
View File
@@ -442,6 +442,27 @@ function NPC:updateTreeShake()
return true return true
end end
function NPC:scriptRockSmash(frames)
-- engine/overworld/map_objects.asm:1462
self.rockSmash = {
frame = 0,
frames = frames or 10,
}
self.frozen = true
return true
end
function NPC:updateRockSmash()
local st = self.rockSmash
if not st then return false end
st.frame = st.frame + 1
if st.frame >= st.frames then
self.rockSmash = nil
return false
end
return true
end
-- `passable` is the follower's escape (src/world/gen2/Follower.lua), the same -- `passable` is the follower's escape (src/world/gen2/Follower.lua), the same
-- name and meaning src/world/Collision.lua:20 gives it under Gen 1. -- name and meaning src/world/Collision.lua:20 gives it under Gen 1.
local function occupied(entities, tx, ty, self) local function occupied(entities, tx, ty, self)
@@ -514,6 +535,10 @@ function NPC:update(map, entities)
self:updateTreeShake() self:updateTreeShake()
return return
end end
if self.rockSmash then
self:updateRockSmash()
return
end
-- NPC_CHANGE_FACING (src/world/NPC.lua:71): one walk cycle in place, no -- NPC_CHANGE_FACING (src/world/NPC.lua:71): one walk cycle in place, no
-- translation. Above the moving arm because it has no targetX to reach, -- translation. Above the moving arm because it has no targetX to reach,
-- and the arm below would assign cellX = nil a frame later. -- and the arm below would assign cellX = nil a frame later.
@@ -726,6 +751,15 @@ function NPC:draw(ox, oy, scale)
self.sprite:draw( self.sprite:draw(
self.px, self.py + yOffset, 0, 0, self.px, self.py + yOffset, 0, 0,
facing, 0, false, false, q == 3) facing, 0, false, false, q == 3)
elseif self.rockSmash then
-- engine/overworld/map_objects.asm:1462
if (self.rockSmash.frame % 2) == 0 then
G.pop()
return
end
self.sprite:draw(
self.px, self.py + yOffset, 0, 0,
self.facing, self:walkPhase(), self.stepFlip)
else else
self.sprite:draw( self.sprite:draw(
self.px, self.py + yOffset, 0, 0, self.px, self.py + yOffset, 0, 0,
+21 -3
View File
@@ -17,6 +17,12 @@ local TURN_FRAMES = 4
-- at the walking rate, which is what stops a bike step flickering the legs. -- at the walking rate, which is what stops a bike step flickering the legs.
Player.STEP_FRAMES = STEP_FRAMES Player.STEP_FRAMES = STEP_FRAMES
-- engine/overworld/map_objects.asm:1815
local JUMP_Y = {
-4, -6, -8, -10, -11, -12, -12, -12,
-11, -10, -9, -8, -6, -4, 0, 0,
}
function Player.new(cx, cy, facing, spriteDef) function Player.new(cx, cy, facing, spriteDef)
local self = setmetatable({ local self = setmetatable({
cellX = cx, cellY = cy, cellX = cx, cellY = cy,
@@ -170,9 +176,11 @@ function Player:update()
self.px = self.cellX * 16 + dx * adv self.px = self.cellX * 16 + dx * adv
self.py = self.cellY * 16 + dy * adv self.py = self.cellY * 16 + dy * adv
if self.jumping then if self.jumping then
-- pokegold engine/overworld/map_objects.asm: UpdateJumpPosition's -- engine/overworld/map_objects.asm:1815
-- y_offsets table peaks at -12. local idx = math.floor((self.progress - 1) / 2) + 1
self.py = self.py - math.floor(12 * math.sin(math.pi * self.progress / frames)) if idx < 1 then idx = 1 end
if idx > #JUMP_Y then idx = #JUMP_Y end
self.spriteYOffset = JUMP_Y[idx]
end end
if self.progress >= frames then if self.progress >= frames then
self.cellX, self.cellY = self.targetX, self.targetY self.cellX, self.cellY = self.targetX, self.targetY
@@ -180,6 +188,7 @@ function Player:update()
self.px, self.py = self.cellX * 16, self.cellY * 16 self.px, self.py = self.cellX * 16, self.cellY * 16
self.moving = false self.moving = false
self.jumping = nil self.jumping = nil
self.spriteYOffset = 0
self.stepFlip = not self.stepFlip self.stepFlip = not self.stepFlip
return true return true
end end
@@ -193,6 +202,15 @@ function Player:draw(ox, oy, scale)
-- standing on. StepFunction_GotBite's `xor 1` rod bob and the fly take-off -- standing on. StepFunction_GotBite's `xor 1` rod bob and the fly take-off
-- lift both ride this one byte. -- lift both ride this one byte.
local yOffset = self.spriteYOffset or 0 local yOffset = self.spriteYOffset or 0
if self.jumping then
-- engine/overworld/map_objects.asm:1995
local gx = ox + self.px * scale
local gy = oy + self.py * scale
local s = 16 * scale
G.setColor(0, 0, 0, 0.4)
G.ellipse("fill", gx + s * 0.5, gy + s * 0.85, s * 0.35, s * 0.12)
G.setColor(1, 1, 1, 1)
end
if self.sprite then if self.sprite then
G.push() G.push()
G.translate(ox, oy) G.translate(ox, oy)
+60 -3
View File
@@ -621,6 +621,8 @@ function World.new(game)
-- which is why a cut tree is back the next time you walk in. Restoring -- which is why a cut tree is back the next time you walk in. Restoring
-- these at the top of setMap is that refill. -- these at the top of setMap is that refill.
blockEdits = {}, blockEdits = {},
-- engine/overworld/map_setup.asm:78
objectSpawns = {},
-- A field move that is mid-flow (the used-X text, then its effect). -- A field move that is mid-flow (the used-X text, then its effect).
fieldMove = nil, fieldMove = nil,
-- ---- state the script VM owns ------------------------------------------ -- ---- state the script VM owns ------------------------------------------
@@ -1819,6 +1821,15 @@ function World:moveObject(objectId, cellX, cellY)
local def = self.map and self.map.def local def = self.map and self.map.def
local obj = def and def.objects and def.objects[index] local obj = def and def.objects and def.objects[index]
if not (obj and cellX and cellY) then return end if not (obj and cellX and cellY) then return end
local mapId = self.map and self.map.id
local key = obj.index or index
if mapId then
self.objectSpawns = self.objectSpawns or {}
self.objectSpawns[mapId] = self.objectSpawns[mapId] or {}
if not self.objectSpawns[mapId][key] then
self.objectSpawns[mapId][key] = { obj.x, obj.y }
end
end
obj.x, obj.y = cellX, cellY obj.x, obj.y = cellX, cellY
local npc = self:objectEntity(objectId) local npc = self:objectEntity(objectId)
if npc and npc ~= self.player then if npc and npc ~= self.player then
@@ -3715,6 +3726,16 @@ function World:updateMovement()
while st.i <= #st.bytes do while st.i <= #st.bytes do
local b = st.bytes[st.i] local b = st.bytes[st.i]
st.i = st.i + 1 st.i = st.i + 1
-- engine/overworld/movement.asm:163
if b == 0x57 then
local duration = st.bytes[st.i] or 0
st.i = st.i + 1
if ent.scriptRockSmash then
ent:scriptRockSmash(duration)
end
st.sleep = duration
return
end
local act = Movement.decodeByte(b) local act = Movement.decodeByte(b)
if act.kind == "end" then if act.kind == "end" then
-- SLIDING_F is an object flag, not a stream one, so a stream that never -- SLIDING_F is an object flag, not a stream one, so a stream that never
@@ -5237,6 +5258,38 @@ function World:restoreBlocks()
return any return any
end end
function World:restoreObjectSpawns()
-- engine/overworld/map_setup.asm:78
local spawns = self.objectSpawns
if not spawns then return end
for mapId, byIndex in pairs(spawns) do
local def = self.maps and self.maps[mapId]
local objects = def and def.objects
if objects then
for key, xy in pairs(byIndex) do
local obj
for _, row in ipairs(objects) do
if (row.index or 0) == key then obj = row break end
end
if obj then
obj.x, obj.y = xy[1], xy[2]
end
local npc = self.npcPool
and self.npcPool[string.format("%s_obj_%d", mapId, key)]
if npc then
npc.cellX, npc.cellY = xy[1], xy[2]
npc.px, npc.py = xy[1] * 16, xy[2] * 16
npc.homeX, npc.homeY = xy[1], xy[2]
npc.moving = false
npc.progress = 0
npc.targetX, npc.targetY = nil, nil
end
end
end
spawns[mapId] = nil
end
end
-- Drop the loaded map's baked canvases and bake again. Same shape as what -- Drop the loaded map's baked canvases and bake again. Same shape as what
-- pollTimeOfDay does when the clock rolls the palette over; a block edit -- pollTimeOfDay does when the clock rolls the palette over; a block edit
-- invalidates the bake for the same reason a palette change does. A world with -- invalidates the bake for the same reason a palette change does. A world with
@@ -5360,8 +5413,10 @@ end
-- picked, and TextBox reads it back off game.stringBuffer. -- picked, and TextBox reads it back off game.stringBuffer.
function World:setNickname(mon) function World:setNickname(mon)
if not self.game then return end if not self.game then return end
self.game.stringBuffer = local name = (mon and (mon.nickname or mon.name or mon.species)) or ""
(mon and (mon.nickname or mon.name or mon.species)) or "" self.game.stringBuffer = name
-- engine/events/overworld.asm:1339
if self.vm then self.vm.stringBuffer = name end
end end
function World:playMonCry(mon) function World:playMonCry(mon)
@@ -8304,6 +8359,7 @@ function World:setMap(mapId, cx, cy, facing, opts)
-- and WHIRLPOOL swapped out goes back: a cut tree is standing again the next -- and WHIRLPOOL swapped out goes back: a cut tree is standing again the next
-- time the map is loaded, and this has to happen before Map.new reads them. -- time the map is loaded, and this has to happen before Map.new reads them.
self:restoreBlocks() self:restoreBlocks()
self:restoreObjectSpawns()
-- HandleNewMap (home/map.asm:216-228) runs ResetMapBufferEventFlags before -- HandleNewMap (home/map.asm:216-228) runs ResetMapBufferEventFlags before
-- anything else that touches state: event flags 0-7 -- anything else that touches state: event flags 0-7
-- (EVENT_TEMPORARY_UNTIL_MAP_RELOAD) die on every map load, which is what -- (EVENT_TEMPORARY_UNTIL_MAP_RELOAD) die on every map load, which is what
@@ -8784,7 +8840,8 @@ function World:tryLedgeJump(dir)
-- ShakeGrass (engine/overworld/movement.asm:741-770). -- ShakeGrass (engine/overworld/movement.asm:741-770).
p.inGrass, p.grassShake = false, nil p.inGrass, p.grassShake = false, nil
p.progress = 0 p.progress = 0
p.stepFrames = Player.STEP_FRAMES -- engine/overworld/map_objects.asm:1163
p.stepFrames = Player.STEP_FRAMES * 2
self:playSfxNamed("Sfx_JumpOverLedge", SFX_JUMP_OVER_LEDGE) self:playSfxNamed("Sfx_JumpOverLedge", SFX_JUMP_OVER_LEDGE)
return true return true
end end
@@ -0,0 +1,49 @@
-- In-game Gold SAVE with no launcher slot must leave a file CONTINUE can see.
--
-- POKEPORT_GAME=gold POKEPORT_IDENTITY=gold-bug1107 \
-- POKEPORT_DRIVER=tests/drivers/gold_save_slot_bug1107_test.lua love .
local U = require("tests.drivers.util")
local SaveData = require("src.core.SaveData")
return function(game)
U.wait(45)
assert(game.world and game.world.map, "gold world did not boot")
local before = SaveData.listSlots("gold")
local hadFile = false
for _, slot in ipairs(before) do
if slot.exists then hadFile = true break end
end
if not hadFile then
local opts = SaveData.loadOptions()
opts.saveSlots = opts.saveSlots or {}
opts.saveSlots.gold = nil
SaveData.saveOptions(opts)
SaveData.resetSlotState()
end
local ok, err = game:writeSave()
if not ok then
U.log("FAIL gold writeSave:", tostring(err))
else
local after = SaveData.listSlots("gold")
local found, path
for _, slot in ipairs(after) do
if slot.exists then
found = slot.id
path = SaveData.slotDiskPath("gold", slot.id)
break
end
end
if found then
U.log("PASS gold save is launcher-visible:", found, path or "")
else
U.log("FAIL gold save wrote but listSlots has no file")
end
end
while true do
coroutine.yield()
end
end
+49
View File
@@ -0,0 +1,49 @@
-- Herb shop intro over the Goldenrod Underground map (#1169).
-- pokegold engine/items/mart.asm:54 HerbShop, maps/GoldenrodUnderground.asm:158
--
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/herbshop_bug1169_test.lua love .
local U = require("tests.drivers.util")
return function(game)
local out = os.getenv("SHOT_DIR") or os.getenv("POKEPORT_SHOT_DIR")
or "/tmp/bug1169"
U.wait(45)
local world = game.world
assert(world and world.map, "gold world did not boot")
-- maps/GoldenrodUnderground.asm:52
world.clockDay = 0
world.mapScenes = world.mapScenes or {}
-- maps/GoldenrodUnderground.asm:679
assert(world:setMap("GOLDENROD_UNDERGROUND", 6, 21, "right"),
"setMap GOLDENROD_UNDERGROUND failed")
U.wait(8)
local granny
for _, npc in ipairs(world.npcs or {}) do
if npc.def and npc.def.sprite == "SPRITE_GRANNY" then granny = npc break end
end
if not granny then
U.log("FAIL granny not on the map (need Sunday)")
end
local MartMenu = require("src.ui.gen2.MartMenu")
U.tap(game, "a")
U.wait(6)
for _ = 1, 90 do
if getmetatable(game.stack:top()) == MartMenu then break end
U.tap(game, "a")
U.wait(4)
end
local top = game.stack:top()
if getmetatable(top) ~= MartMenu then
U.log("FAIL mart did not open")
elseif top.isOpaque then
U.log("FAIL mart is opaque")
else
U.log("herb shop intro over the map")
end
U.shot(game, out .. "/herbshop_intro.png")
while true do U.wait(60) end
end
+88
View File
@@ -0,0 +1,88 @@
-- Kurt stays at moveobject (11, 6) for the visit, then ROM spawn (16, 14)
-- on the next load.
-- #1184
-- maps/SlowpokeWellB1F.asm:48 / engine/overworld/map_setup.asm:78
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/kurt_well_bug1184_test.lua love .
local U = require("tests.drivers.util")
local KURT_ID = 8
local ROM_X, ROM_Y = 16, 14
local MOVE_X, MOVE_Y = 11, 6
local FLAG_KURT = 1856
local FLAG_ROCKETS = 1788
local function findKurt(world)
for _, npc in ipairs(world.npcs or {}) do
if npc.def and npc.def.sprite == "SPRITE_KURT" then return npc end
end
return nil
end
local function kurtDef(world)
local objects = world.map and world.map.def and world.map.def.objects
return objects and objects[KURT_ID - 1]
end
return function(game)
U.wait(45)
local world = game.world
assert(world and world.map, "gold world did not boot")
world.mapScenes = world.mapScenes or {}
world.mapScenes.PLAYERS_HOUSE_1F = 1
world.mapScenes.NEW_BARK_TOWN = 1
world.events:set(FLAG_KURT, false)
world.events:set(FLAG_ROCKETS, true)
assert(world:setMap("SLOWPOKE_WELL_B1F", 15, 14, "right"),
"SLOWPOKE_WELL_B1F did not load")
U.wait(4)
local pass, fail = 0, 0
local function claim(ok, text)
if ok then pass = pass + 1 else fail = fail + 1 end
U.log(ok and "PASS" or "FAIL", text)
end
local kurt = findKurt(world)
claim(kurt ~= nil, "Kurt is on the well map")
claim(kurt and kurt.cellX == ROM_X and kurt.cellY == ROM_Y,
("Kurt ROM spawn is (%d, %d), saw (%s, %s)")
:format(ROM_X, ROM_Y,
tostring(kurt and kurt.cellX), tostring(kurt and kurt.cellY)))
world:moveObject(KURT_ID, MOVE_X, MOVE_Y)
world:appearObject(KURT_ID)
U.wait(4)
kurt = findKurt(world)
claim(kurt and kurt.cellX == MOVE_X and kurt.cellY == MOVE_Y,
("after moveobject Kurt is at (%d, %d), saw (%s, %s)")
:format(MOVE_X, MOVE_Y,
tostring(kurt and kurt.cellX), tostring(kurt and kurt.cellY)))
assert(world:setMap("PLAYERS_HOUSE_1F", 3, 3, "down"),
"PLAYERS_HOUSE_1F did not load")
U.wait(2)
world.events:set(FLAG_KURT, false)
assert(world:setMap("SLOWPOKE_WELL_B1F", 15, 14, "right"),
"SLOWPOKE_WELL_B1F did not reload")
U.wait(4)
local def = kurtDef(world)
kurt = findKurt(world)
claim(def and def.x == ROM_X and def.y == ROM_Y,
("reload restored Kurt def to (%d, %d), saw (%s, %s)")
:format(ROM_X, ROM_Y,
tostring(def and def.x), tostring(def and def.y)))
claim(kurt and kurt.cellX == ROM_X and kurt.cellY == ROM_Y,
("reload put Kurt at (%d, %d), not the blocking cell (%d, %d); saw (%s, %s)")
:format(ROM_X, ROM_Y, MOVE_X, MOVE_Y,
tostring(kurt and kurt.cellX), tostring(kurt and kurt.cellY)))
U.log(("machine checks: %d passed, %d failed"):format(pass, fail))
U.log("Kurt should be at the well entrance, not on the inner path.")
while true do U.wait(60) end
end
+143
View File
@@ -0,0 +1,143 @@
-- Fury Attack / Barrage hit 2-5 times, Twineedle stays at 2. Issue #1168.
-- pokegold engine/battle/effect_commands.asm:5228 (BattleCommand_EndLoop).
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/multihit_bug1168_test.lua love .
-- Do not add POKEPORT_SPEED: the per-strike HP steps are what you are judging.
local U = require("tests.drivers.util")
local Battle = require("src.battle.gen2.Battle")
local Effects = require("src.battle.gen2.Effects")
local Mon = require("src.battle.gen2.Mon")
return function(game)
local function tap(button, frames)
game.input.pressQueue[#game.input.pressQueue + 1] = button
game.input.state[button] = true
U.wait(2)
game.input.state[button] = false
U.wait(frames or 6)
end
local function claim(ok, text)
print((ok and "[multihit] PASS " or "[multihit] FAIL ") .. text)
end
U.wait(45)
local world = game.world
if not (world and world.map) then
print("[multihit] FAIL gold world did not boot")
while true do U.wait(60) end
end
local fury = game.data.moves and game.data.moves.FURY_ATTACK
local twine = game.data.moves and game.data.moves.TWINEEDLE
claim(fury ~= nil, "FURY_ATTACK is in the move table")
claim(fury and fury.effect == "EFFECT_MULTI_HIT",
"FURY_ATTACK is EFFECT_MULTI_HIT")
claim(twine ~= nil, "TWINEEDLE is in the move table")
claim(twine and twine.effect == "EFFECT_POISON_MULTI_HIT",
"TWINEEDLE is EFFECT_POISON_MULTI_HIT")
claim(Effects.hitCount("EFFECT_DOUBLE_HIT") == 2, "DOUBLE_HIT is 2")
claim(Effects.hitCount("EFFECT_POISON_MULTI_HIT") == 2,
"POISON_MULTI_HIT (Twineedle) is 2")
local seq, si = { 0, 1, 2, 0, 3, 3 }, 0
local function scripted()
si = si + 1
return seq[si] or 0
end
claim(Effects.hitCount("EFFECT_MULTI_HIT", scripted) == 2, "roll 0 -> 2 hits")
claim(Effects.hitCount("EFFECT_MULTI_HIT", scripted) == 3, "roll 1 -> 3 hits")
claim(Effects.hitCount("EFFECT_MULTI_HIT", scripted) == 2,
"roll 2 then 0 -> 2 hits")
claim(Effects.hitCount("EFFECT_MULTI_HIT", scripted) == 5,
"roll 3 then 3 -> 5 hits")
local function fiveHitRandom(n)
if n == 4 then return 3 end
return 0
end
local headP = Mon.new(game.data, "CYNDAQUIL", 20)
local headW = Mon.new(game.data, "SNORLAX", 20)
if headP and headW then
headP.moves = { { id = "FURY_ATTACK", pp = 20, maxPp = 20 } }
local b = Battle.new({
data = game.data, party = { headP }, wild = headW,
random = fiveHitRandom,
})
local landed, hitLine = 0, nil
for _, ev in ipairs(b:takeTurn({ kind = "move", move = "FURY_ATTACK" })) do
if ev.kind == "damage" and ev.side == "enemy" then
landed = landed + 1
end
if ev.kind == "message" and ev.text then
local n = ev.text:match("Hit (%d+) time")
if n then hitLine, landed = ev.text, tonumber(n) end
end
end
claim(landed == 5,
("scripted Fury Attack hit %d times (want 5, not 2)"):format(landed))
if hitLine then print("[multihit] " .. hitLine) end
else
claim(false, "could not build a headless Fury Attack pair")
end
local player = Mon.new(game.data, "CYNDAQUIL", 20)
if not player then
print("[multihit] FAIL could not build CYNDAQUIL")
while true do U.wait(60) end
end
player.moves = { { id = "FURY_ATTACK", pp = 20, maxPp = 20 } }
game.save.party = { player }
local wild = Mon.new(game.data, "SNORLAX", 20)
if not wild then
print("[multihit] FAIL could not build SNORLAX")
while true do U.wait(60) end
end
if not world:startBattle({ wild = wild }) then
print("[multihit] FAIL startBattle failed")
while true do U.wait(60) end
end
local screen
for _ = 1, 600 do
local top = game.stack:top()
if top and top.battle then screen = top break end
U.wait(1)
end
if not (screen and screen.battle) then
print("[multihit] FAIL battle screen never came up")
while true do U.wait(60) end
end
local left = 2
screen.battle.random = function(n)
if n == 4 and left > 0 then
left = left - 1
return 3
end
if left > 0 then return 0 end
if love and love.math and love.math.random then
return love.math.random(n) - 1
end
return math.random(n) - 1
end
for _ = 1, 200 do
if screen.phase == "menu" then break end
tap("a", 3)
end
if screen.phase ~= "menu" then
print("[multihit] FAIL never reached the battle menu")
while true do U.wait(60) end
end
tap("a")
U.wait(6)
tap("a")
print("[multihit] Fury Attack should strike five times on this turn.")
print("[multihit] later turns are 2-5. Twineedle would stay at 2.")
while true do U.wait(60) end
end
@@ -0,0 +1,92 @@
-- Radio Tower 5F director returns to the office after a downstairs/upstairs
-- reload, not on the stair warp at (12, 0).
-- #1164 / #1188
-- maps/RadioTower5F.asm:115 / engine/overworld/map_setup.asm:78
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/radiotower_softlock_bug1164_test.lua love .
local U = require("tests.drivers.util")
local DIRECTOR_ID = 2
local OFFICE_X, OFFICE_Y = 3, 6
local STAIR_X, STAIR_Y = 12, 0
local FLAG_ROCKETS = 1742
local FLAG_CIVILIANS = 1744
local function findDirector(world)
for _, npc in ipairs(world.npcs or {}) do
if npc.def and npc.def.sprite == "SPRITE_GENTLEMAN" then return npc end
end
return nil
end
local function directorDef(world)
local objects = world.map and world.map.def and world.map.def.objects
return objects and objects[DIRECTOR_ID - 1]
end
return function(game)
U.wait(45)
local world = game.world
assert(world and world.map, "gold world did not boot")
world.mapScenes = world.mapScenes or {}
world.mapScenes.RADIO_TOWER_5F = 2
world.mapScenes.RADIO_TOWER_4F = 0
world.events:set(FLAG_ROCKETS, true)
world.events:set(FLAG_CIVILIANS, false)
assert(world:setMap("RADIO_TOWER_5F", 10, 4, "down"),
"RADIO_TOWER_5F did not load")
U.wait(4)
local pass, fail = 0, 0
local function claim(ok, text)
if ok then pass = pass + 1 else fail = fail + 1 end
U.log(ok and "PASS" or "FAIL", text)
end
local director = findDirector(world)
claim(director ~= nil, "director is on 5F")
claim(director and director.cellX == OFFICE_X and director.cellY == OFFICE_Y,
("director office spawn is (%d, %d), saw (%s, %s)")
:format(OFFICE_X, OFFICE_Y,
tostring(director and director.cellX),
tostring(director and director.cellY)))
world:moveObject(DIRECTOR_ID, STAIR_X, STAIR_Y)
world:appearObject(DIRECTOR_ID)
U.wait(4)
director = findDirector(world)
claim(director and director.cellX == STAIR_X and director.cellY == STAIR_Y,
("after moveobject director is at the stairs (%d, %d), saw (%s, %s)")
:format(STAIR_X, STAIR_Y,
tostring(director and director.cellX),
tostring(director and director.cellY)))
assert(world:setMap("RADIO_TOWER_4F", 12, 4, "down"),
"RADIO_TOWER_4F did not load")
U.wait(2)
assert(world:setMap("RADIO_TOWER_5F", 10, 4, "down"),
"RADIO_TOWER_5F did not reload")
U.wait(4)
local def = directorDef(world)
director = findDirector(world)
claim(def and def.x == OFFICE_X and def.y == OFFICE_Y,
("reload restored director def to (%d, %d), saw (%s, %s)")
:format(OFFICE_X, OFFICE_Y,
tostring(def and def.x), tostring(def and def.y)))
claim(not (director and director.cellX == STAIR_X and director.cellY == STAIR_Y),
"director is not standing on the stair warp")
claim(director and director.cellX == OFFICE_X and director.cellY == OFFICE_Y,
("reload put director in the office (%d, %d); saw (%s, %s)")
:format(OFFICE_X, OFFICE_Y,
tostring(director and director.cellX),
tostring(director and director.cellY)))
U.log(("machine checks: %d passed, %d failed"):format(pass, fail))
U.log("The gentleman should be in the office. The stairs must be clear.")
while true do U.wait(60) end
end
@@ -0,0 +1,85 @@
-- Route 40 smashable rocks stay on their cell through rock_smash, then a
-- seamless Olivine round trip.
-- #1173
-- engine/overworld/movement.asm:163 / maps/Route40.asm:291
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/rocksmash_shift_bug1173_test.lua love .
local U = require("tests.drivers.util")
local ROCK_ID = 6
local ROCK_X, ROCK_Y = 12, 8
local SMASH = { 0x57, 10, 0x47 }
local function findRock(world, x, y)
for _, npc in ipairs(world.npcs or {}) do
if npc.def and npc.def.sprite == "SPRITE_ROCK"
and npc.cellX == x and npc.cellY == y then
return npc
end
end
return nil
end
local function rockAt(world, x, y)
for _, npc in ipairs(world.npcs or {}) do
if npc.def and npc.def.sprite == "SPRITE_ROCK"
and npc.cellX == x and npc.cellY == y then
return true
end
end
return false
end
return function(game)
U.wait(45)
local world = game.world
assert(world and world.map, "gold world did not boot")
world.mapScenes = world.mapScenes or {}
world.mapScenes.ROUTE_40 = 0
world.mapScenes.OLIVINE_CITY = 0
assert(world:setMap("ROUTE_40", 12, 9, "up"), "ROUTE_40 did not load")
U.wait(4)
local pass, fail = 0, 0
local function claim(ok, text)
if ok then pass = pass + 1 else fail = fail + 1 end
U.log(ok and "PASS" or "FAIL", text)
end
local rock = findRock(world, ROCK_X, ROCK_Y)
claim(rock ~= nil, ("rock is at (%d, %d)"):format(ROCK_X, ROCK_Y))
world:beginMovement(ROCK_ID, SMASH)
for _ = 1, 40 do
if not world.moveState then break end
U.wait(1)
end
claim(world.moveState == nil, "rock_smash stream finished")
rock = findRock(world, ROCK_X, ROCK_Y)
claim(rock ~= nil,
("after rock_smash the rock is still at (%d, %d), not one cell left")
:format(ROCK_X, ROCK_Y))
claim(not rockAt(world, ROCK_X - 1, ROCK_Y),
"no smashable rock slid one cell left")
assert(world:setMap("OLIVINE_CITY", 18, 15, "down", { seamless = true }),
"OLIVINE_CITY did not load")
U.wait(2)
assert(world:setMap("ROUTE_40", 12, 9, "up", { seamless = true }),
"ROUTE_40 did not reload")
U.wait(4)
claim(rockAt(world, ROCK_X, ROCK_Y),
("after Olivine round trip the rock is still at (%d, %d)")
:format(ROCK_X, ROCK_Y))
claim(not rockAt(world, ROCK_X - 1, ROCK_Y),
"seamless reload did not shift the rock left")
U.log(("machine checks: %d passed, %d failed"):format(pass, fail))
U.log("The three rocks should sit on (12, 8), (11, 7), (13, 6).")
while true do U.wait(60) end
end
@@ -0,0 +1,94 @@
-- After a wild faint, "Use next POKéMON?" yes/no; NO tries to run. Issue #1152.
-- pokegold engine/battle/core.asm:2590 (AskUseNextPokemon).
-- POKEPORT_GAME=gold POKEPORT_DRIVER=tests/drivers/use_next_mon_bug1152_test.lua love .
-- Do not add POKEPORT_SPEED: you need the yes/no box to sit there.
local U = require("tests.drivers.util")
local Mon = require("src.battle.gen2.Mon")
return function(game)
local function tap(button, frames)
game.input.pressQueue[#game.input.pressQueue + 1] = button
game.input.state[button] = true
U.wait(2)
game.input.state[button] = false
U.wait(frames or 6)
end
local function claim(ok, text)
print((ok and "[use-next] PASS " or "[use-next] FAIL ") .. text)
return ok
end
U.wait(45)
local world = game.world
if not (world and world.map) then
print("[use-next] FAIL gold world did not boot")
while true do U.wait(60) end
end
local lead = Mon.new(game.data, "SENTRET", 5)
local backup = Mon.new(game.data, "CYNDAQUIL", 12)
if not (lead and backup) then
print("[use-next] FAIL could not build the party")
while true do U.wait(60) end
end
lead.hp = 1
lead.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
backup.moves = { { id = "TACKLE", pp = 35, maxPp = 35 } }
game.save.party = { lead, backup }
local wild = Mon.new(game.data, "PIDGEY", 20)
if not wild then
print("[use-next] FAIL could not build a wild PIDGEY")
while true do U.wait(60) end
end
if not world:startBattle({ wild = wild }) then
print("[use-next] FAIL startBattle failed")
while true do U.wait(60) end
end
local screen
for _ = 1, 600 do
local top = game.stack:top()
if top and top.battle then screen = top break end
U.wait(1)
end
if not (screen and screen.battle) then
print("[use-next] FAIL battle screen never came up")
while true do U.wait(60) end
end
for _ = 1, 200 do
if screen.phase == "menu" then break end
tap("a", 3)
end
if screen.phase ~= "menu" then
print("[use-next] FAIL never reached the battle menu")
while true do U.wait(60) end
end
tap("a")
U.wait(6)
tap("a")
local sawAsk, sawForced = false, false
for _ = 1, 400 do
if screen.phase == "ask-next-mon" then sawAsk = true break end
if screen.phase == "forced-switch" or screen.phase == "submenu" then
sawForced = true
break
end
if screen.battle.over then break end
tap("a", 3)
end
claim(sawAsk, "wild faint opened Use next POKéMON?")
claim(not sawForced, "wild faint did not skip to the party list")
claim(screen.phase == "ask-next-mon",
"phase is ask-next-mon (YES switches, NO/B tries to run)")
print("[use-next] YES sends you to the party. NO or B tries to run.")
print("[use-next] a failed run still opens the party. trainers skip this.")
while true do U.wait(60) end
end