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
+14 -5
View File
@@ -217,7 +217,7 @@ function Battle.new(opts)
opts = opts or {}
local self = setmetatable({}, Battle)
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.trainer = opts.trainer
self.save = opts.save
@@ -271,6 +271,13 @@ function Battle.new(opts)
self.enemy = self.enemyParty[self.enemyIndex]
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
-- NewEnemyMonStatus run at the first send-out of every battle.
self:clearAllVolatiles()
@@ -1655,7 +1662,7 @@ function Battle:useMove(attacker, defender, moveId)
state.rolloutLock = nil
end
local hits = Effects.hitCount(def.effect, self.random)
local hits = Effects.hitCount(def.effect, self:roller())
local landed = 0
for hit = 1, hits do
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.
Happiness.change(mon, "GAINLEVEL")
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
local ok, reason, entry = Mon.learnMove(mon, moveId, self.data)
local moveDef = self:moveDef(moveId)
@@ -3644,7 +3652,7 @@ end
-- 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.
-- Trainers never let you run.
function Battle:tryRun()
function Battle:tryRun(pSpd)
-- .cant_escape and .cant_run_from_trainer leave wBattlePlayerAction alone,
-- 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
@@ -3676,7 +3684,8 @@ function Battle:tryRun()
return false
end
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:emit({ kind = "run", text = "Got away safely!" })
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
-- fallthrough in .DetermineNumberOfHits produces.
function Effects.multiHitCount(random)
local roll = random and random(4) or 0
if roll < 2 then return roll + 2 end
-- Hits 4 and 5 take a second roll, so each ends up half as likely.
local second = random and random(2) or 0
return second + 4
local function roll(n)
if random then return random(n) end
if love and love.math and love.math.random then
return love.math.random(n) - 1
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
Effects.HIT_COUNTS = {
EFFECT_DOUBLE_HIT = 2,
EFFECT_POISON_MULTI_HIT = 2,
-- Triple Kick stops early if a hit misses; Battle rolls that per hit.
EFFECT_TRIPLE_KICK = 3,
}
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)
end
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 {}
dvs = dvs 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
+ math.floor(math.sqrt(statExp.hp or 0) / 4)) * level) / 100)
+ 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
-- per-stat keys are still read as a fallback for a record written before
-- the shared word existed.
specialAttack = statValue(baseStats.specialAttack, dvs.special, level,
specialAttack = statValue(baseStats.specialAttack, specialDv, level,
statExp.special or statExp.specialAttack),
specialDefense = statValue(baseStats.specialDefense, dvs.special, level,
specialDefense = statValue(baseStats.specialDefense, specialDv, level,
statExp.special or statExp.specialDefense),
}
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.
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)
-- src/core/Game.lua:1121 mirrors this call for Gen 1
Input:applyBindings(options.bindings)
-- options.touchControls (the launcher editor's per-orientation layouts) and
-- options.haptics, the same two keys Gen 1 hands over here
-- (src/core/Game.lua:1073). One options.lua serves both games, so the pad a
-- player laid out for Red is already the pad Gold draws.
TouchControls:applyOptions(options)
TouchControls:applyOptions({
touchControls = options.touchControls,
haptics = options.haptics,
})
local GBCFX = require("src.render.GBCFX")
if GBCFX.applyOptions(options) and self.save then
-- 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.stickDir = nil
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
-- Multiple physical sources (W + Up, d-pad + stick, etc.) can claim the
@@ -154,6 +182,7 @@ local function release(self, btn, source)
end
function Input:keypressed(key)
noteCapture(self, "key", "pressed", key)
local btn = self.keyBindings[key]
if btn then
press(self, btn, "key:" .. key)
@@ -161,6 +190,7 @@ function Input:keypressed(key)
end
function Input:keyreleased(key)
noteCapture(self, "key", "released", key)
local btn = self.keyBindings[key]
if btn then
release(self, btn, "key:" .. key)
@@ -221,6 +251,7 @@ function Input:sourceRelease(btn, source)
end
function Input:gamepadpressed(joystick, button)
noteCapture(self, "pad", "pressed", button)
local btn = self.padBindings[button]
if btn then
press(self, btn, "pad:" .. button)
@@ -228,6 +259,7 @@ function Input:gamepadpressed(joystick, button)
end
function Input:gamepadreleased(joystick, button)
noteCapture(self, "pad", "released", button)
local btn = self.padBindings[button]
if btn then
release(self, btn, "pad:" .. button)
@@ -249,12 +281,14 @@ end
function Input:joystickpressed(joystick, button)
if GamepadMap.ignoreRawForJoystick(joystick) then return end
noteCapture(self, "joy", "pressed", button)
local btn = self.joyBindings[button]
if btn then press(self, btn, "joy:" .. button) end
end
function Input:joystickreleased(joystick, button)
if GamepadMap.ignoreRawForJoystick(joystick) then return end
noteCapture(self, "joy", "released", button)
local btn = self.joyBindings[button]
if btn then release(self, btn, "joy:" .. button) end
end
+4
View File
@@ -211,6 +211,10 @@ function TouchControls.defaultLayout(ww, wh, ox, oy, scale)
local abW = dpadW * 0.46
local ssW = dpadW * 0.30
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 {
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 },
+9
View File
@@ -275,6 +275,8 @@ Save.DEFAULT_OPTIONS = {
musicVol = 7, -- 0-7, like the GB's NR50 master volume
sfxVol = 7, -- 0-7
musicFilter = 0, -- low-pass steps, 0 = off
haptics = "light",
touchControls = { enabled = true },
}
function Save.defaultOptions()
@@ -719,6 +721,13 @@ end
-- copy is the witness that survives a crash mid-replace.
function Save.save(save)
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 f = fs()
if not f then return false, "no filesystem" end
+41 -2
View File
@@ -1370,6 +1370,10 @@ function RomImporter.new(onComplete, opts)
end
end
if GameVersion.VERSIONS[self.tab] then
self.modScope = self.tab
end
return self
end
@@ -2627,6 +2631,9 @@ function RomImporter:_switchTab(id)
self.tab = id
self._findSearchFocus = false
self:_disarmTextInput()
if GameVersion.VERSIONS[id] then
self:_setModScope(id)
end
end
function RomImporter:_toggleFindSearchFocus()
@@ -2937,6 +2944,13 @@ function RomImporter:_refreshMods()
end
end
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)
end
@@ -3586,7 +3600,7 @@ function RomImporter:_findRows()
-- index, query, or category actually changed.
local c = self._findRowsCache
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
end
local ModIndex = require("src.mods.ModIndex")
@@ -3594,8 +3608,33 @@ function RomImporter:_findRows()
query = self.findQuery,
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,
category = self.findCategory, rows = rows }
category = self.findCategory, scope = self.modScope, rows = rows }
return rows
end
+7 -1
View File
@@ -456,7 +456,13 @@ end
-- AskRockSmashScript reads it with `ifequal 1, .no`. Transcribing it the
-- obvious way round refuses the move for every party that has it.
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
-- 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.onGamepadReleased = BindingsMenu.capturePadRelease
self.onJoystickReleased = BindingsMenu.captureJoyRelease
if Input.armCapture then Input:armCapture() end
end
function BindingsMenu:endCapture()
@@ -138,6 +139,7 @@ function BindingsMenu:endCapture()
self.onKeyReleased = nil
self.onGamepadReleased = nil
self.onJoystickReleased = nil
if Input.disarmCapture then Input:disarmCapture() end
end
-- 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
opts.bindings[item.button.id] = b
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
-- SELECT: forget one row's rebind and fall back to the defaults. #510's
@@ -246,7 +252,11 @@ function BindingsMenu:clearBinding(item)
end
opts.bindings[item.button.id] = nil
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
-- 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
it.right = boundRight(nil, it.button)
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
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)
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
return self:confirmReset()
end
+94 -7
View File
@@ -49,17 +49,29 @@ end
function Editor.load(opts)
opts = opts or {}
Editor.onClose = opts.onClose
Editor.version = opts.version
Editor.hostPoll = opts.hostPoll == true
Editor.drag = nil
Editor.rects = {}
Editor._hostMouse = false
Editor._hostTouches = nil
Editor.fonts = {
title = love.graphics.newFont(28),
body = love.graphics.newFont(16),
btn = love.graphics.newFont(18),
}
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:ensureImages()
TouchControls:applyOptions(optsTbl)
TouchControls:applyOptions(applied)
TouchControls:setPreview(true)
Editor.enabled = TouchControls.enabled ~= false
PadCursor.reset()
@@ -71,17 +83,24 @@ function Editor.unload()
PadCursor.reset()
Editor.drag = nil
Editor.onClose = nil
Editor.version = nil
Editor._hostMouse = false
Editor._hostTouches = nil
end
local function persist()
local opts = SaveData.loadOptions()
local cfg = TouchControls:config()
-- replaces the whole table, so a pre-#633 top-level positions key is
-- dropped once the player saves under the new shape
opts.touchControls = {
local block = {
enabled = cfg.enabled,
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)
end
@@ -104,9 +123,7 @@ end
function Editor.update(dt)
PadCursor.update(dt or 0)
-- drag follows the live pointer when love.touch / mouse / pad is available;
-- touchmoved / mousemoved also update, so this is a belt-and-suspenders
-- path for Android where move events can be thin
if Editor.hostPoll then Editor.pollHostPointers() end
if not Editor.drag then return end
local x, y
if Editor.drag.touchId == "pad" then
@@ -297,6 +314,45 @@ local function endDrag(id)
Editor.drag = nil
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)
if button ~= 1 then return end
-- Finger / mouse tap yields the Joy-Con pointer so the click lands where
@@ -391,4 +447,35 @@ function Editor.keypressed(key)
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
+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).
local TEXT_NO_WILL_TO_FIGHT = "There's no will to 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
-- (data/text/battle.asm:315-322).
@@ -1146,11 +1148,11 @@ function BattleState:advanceQueue()
-- pokegold engine/battle/core.asm:7057-7069: every mon that leveled
-- gets the stats box, not just the mon currently on the field.
self.pendingStatsMon = mon
-- BattleText_StringBuffer1GrewToLevel ends in text_end (battle.asm:336-343),
-- 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.
-- engine/battle/core.asm:7044
if mon and mon == battle.player then
event.text = nil
event.sfx = nil
event.waitSfx = nil
if self.shownHp then
self.shownHp.player = mon.hp or 0
if self.hpAnim and self.hpAnim.side == "player" then
@@ -1270,6 +1272,14 @@ function BattleState:advanceQueue()
return self:askNickname(event.mon)
end
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.
self.phase = "forced-switch"
self.message = "Choose a POKéMON."
@@ -1876,6 +1886,36 @@ function BattleState:update(_dt)
return
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.messageTimer > 0 then
if input:wasPressed("a") or input:wasPressed("b") then
@@ -2433,6 +2473,31 @@ function BattleState:offerShiftSwitch(mon)
self.messageTimer = MESSAGE_FRAMES
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
-- PARTYMENUACTION_SWITCH and carries no submenu; a cancel is `.canceled_switch`
-- and answers exactly like NO (:3327).
@@ -3173,15 +3238,18 @@ function BattleState:drawPanel()
-- stands.
local asking = self.phase == "ask-nickname" or self.phase == "ask-forget"
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
-- 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).
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.print("YES", left + 2, 8)
Chrome.print("NO", left + 2, 10)
local index = self.phase == "ask-nickname" and self.nicknameIndex
or self.phase == "ask-shift" and self.shiftIndex
or self.phase == "ask-next-mon" and self.nextMonIndex
or self.forgetChoice
Chrome.cursor(left + 1, index == 1 and 8 or 10)
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).
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
Chrome.textbox(9, 0, 9, 10)
for i, row in ipairs(STATS_BOX_ROWS) do
+4 -22
View File
@@ -75,7 +75,8 @@ local SFX_TRANSACTION = "Sfx_Transaction"
local 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
-- ahead of the word mart id, and MartTypeDialogs is indexed by it.
@@ -330,9 +331,6 @@ local function extractedText(text, base, labels)
return out
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/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
@@ -896,7 +894,8 @@ local function printPriceOpaque(amount, ty)
end
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
local i = row + self.scroll
local ty = LIST_Y + (row - 1) * LIST_SPACING
@@ -953,18 +952,14 @@ end
function MartMenu:drawUnder()
local phase = self.phase
if phase == "top" then
Chrome.clear()
self:drawTopMenu()
self:drawTextBox(self.topLines)
elseif phase == "buy" or phase == "buyQuantity" then
Chrome.clear()
self:drawMoneyBox()
self:drawBuyList()
self:drawDescription()
elseif phase == "sell" or phase == "sellQuantity" then
if self.pack then self.pack:drawPanel() end
else
Chrome.clear()
end
end
@@ -1003,17 +998,4 @@ function MartMenu:draw()
self:drawPanel()
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
+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
-- one it would draw in raw DMG shades next to a colored world.
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 = {}
if self.gfx then
for _, key in ipairs({ "border", "middleLine", "underLine", "cursor" }) do
@@ -315,6 +321,10 @@ function NamingScreen:update(_dt)
end
end
function NamingScreen:paperColor()
return GbcPalette.color(self.palette, 1)
end
-- 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
-- cleared panels readable.
@@ -322,17 +332,25 @@ function NamingScreen:drawBackdrop()
local G = love.graphics
local tile = self.tiles.border
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.setColor(1, 1, 1, 1)
return
end
G.setColor(1, 1, 1, 1)
for ty = 0, Chrome.SCREEN_H - 1 do
for tx = 0, Chrome.SCREEN_W - 1 do
G.draw(tile, tx * 8, ty * 8)
local function blit()
G.setColor(1, 1, 1, 1)
for ty = 0, Chrome.SCREEN_H - 1 do
for tx = 0, Chrome.SCREEN_W - 1 do
G.draw(tile, tx * 8, ty * 8)
end
end
end
if self.palette and GbcPalette.available() then
GbcPalette.with(self.palette, blit)
else
blit()
end
end
-- The cursor (data/sprite_anims/oam.asm .OAMData_TextEntryCursor and
@@ -410,7 +428,8 @@ end
function NamingScreen:clearPanel(tx, ty, tw, th)
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.setColor(0, 0, 0, 1)
end
@@ -472,12 +491,13 @@ function NamingScreen:drawPanel()
G.draw(self.iconImage, quad, 16, 16)
end
end
local pal = self.palette
if self.monName then
-- Nickname header is two lines: "<MON>'S" then "NICKNAME?".
Chrome.print(self.monName .. "'S", 5, 2)
Chrome.print("NICKNAME?", 5, 4)
Chrome.printThrough(self.monName .. "'S", 5, 2, pal)
Chrome.printThrough("NICKNAME?", 5, 4, pal)
else
Chrome.print(self.prompt, 5, 2)
Chrome.printThrough(self.prompt, 5, 2, pal)
end
self:drawEntry(5, self.isBox and 4 or 6)
@@ -489,17 +509,22 @@ function NamingScreen:drawPanel()
for col = 0, 8 do
local ch = line[col + 1]
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
local labels = self.lower and BOTTOM_LOWER_LABELS or BOTTOM_UPPER_LABELS
local bottomY = keyboardTop + bottom * 2
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
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)
end
@@ -511,23 +536,32 @@ function NamingScreen:drawWidescreen(winW, winH)
local G = love.graphics
-- The naming screen's own patterned backdrop is the surround: extend it to
-- the window edges so a widescreen boot has no black pillarbox.
self:drawBackdrop()
local scale = Chrome.fitScale(winW, winH)
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.setColor(1, 1, 1, 1)
if self.tiles.border then
local tilesX = math.ceil(winW / (8 * scale))
local tilesY = math.ceil(winH / (8 * scale))
G.push()
G.scale(scale, scale)
for ty = 0, tilesY do
for tx = 0, tilesX do
G.draw(self.tiles.border, tx * 8, ty * 8)
local tile = self.tiles.border
local function blit()
G.setColor(1, 1, 1, 1)
G.push()
G.scale(scale, scale)
for ty = 0, tilesY do
for tx = 0, tilesX do
G.draw(tile, tx * 8, ty * 8)
end
end
G.pop()
end
if self.palette and GbcPalette.available() then
GbcPalette.with(self.palette, blit)
else
blit()
end
G.pop()
end
G.push()
G.translate(ox, oy)
+42 -4
View File
@@ -172,6 +172,34 @@ local ROWS = {
text = function(options)
return require("src.render.GBCFX").levelLabel(options.gbcfx or 0)
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 },
}
@@ -196,11 +224,18 @@ local function sameRows(_, rows) return rows end
-- COLOR) simply appear in the list the hook receives.
local function buildRows()
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
local 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[i] = copy
if showTouch or (row.id ~= "touchControls" and row.id ~= "touchLayout"
and row.id ~= "haptics") then
local 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
return rows
end
@@ -291,6 +326,9 @@ end
function OptionsMenu:leave_()
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
function OptionsMenu:update(_dt)
+66
View File
@@ -354,6 +354,12 @@ function PackMenu:useSelected()
end
return
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
-- four entries are all .Oak -- an X ATTACK or a POKé DOLL used from the
-- field PACK prints OakThisIsntTheTimeText and goes nowhere. Only the
@@ -586,6 +592,66 @@ function PackMenu:giveToSlot(slot, row)
held:giveItem(row.id)
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)
local input = self.game and self.game.input
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 Logger = require("src.core.Logger")
local Mail = require("src.core.gen2.Mail")
local Mon = require("src.battle.gen2.Mon")
local Runtime = require("src.mods.Runtime")
local Screens = require("src.ui.Screens")
@@ -90,11 +91,20 @@ function PartyMenu.new(game, opts)
self.save = save
self.party = opts.party or (save and save.party) 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.palettes = opts.palettes or data.gen2Palettes
self.pokemon = opts.pokemon or data.pokemon
self.prompt = PartyMenu.PROMPTS[opts.prompt or "choose"] or opts.prompt
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.onCancel = opts.onCancel
self.moves = opts.moves or data.moves
@@ -469,7 +479,14 @@ function PartyMenu:update(_dt)
elseif self.wantsSubmenu or self.wantsBattleSubmenu then
self:openSubmenu()
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
elseif input:wasPressed("b") then
self:storeCursor()
@@ -674,6 +691,18 @@ function PartyMenu.rowFor(mon)
}
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
-- below is the hlcoord the matching PARTYMENUQUALITY_* routine uses, and each
-- 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))
local row = PartyMenu.rowFor(mon)
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.level then Chrome.print(row.level, 8, dataY) end
if row.hp then self:drawHpBar(mon, 11, dataY) end
end
-- .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.
if self.fly then return self:updateFlyMap(input) end
if self.mode == "strip" then
if input:wasPressed("left") then
self.cardIndex = self.cardIndex > 1 and self.cardIndex - 1 or #self.cards
elseif input:wasPressed("right") then
self.cardIndex = self.cardIndex < #self.cards and self.cardIndex + 1 or 1
elseif input:wasPressed("a") then
self.mode = "card"
elseif input:wasPressed("b") then
if self.onClose then self.onClose() end
local stripCard = self:card()
if not (stripCard and stripCard.id == "phone") then
if input:wasPressed("left") then
self.cardIndex = self.cardIndex > 1 and self.cardIndex - 1 or #self.cards
elseif input:wasPressed("right") then
self.cardIndex = self.cardIndex < #self.cards and self.cardIndex + 1 or 1
elseif input:wasPressed("a") then
self.mode = "card"
elseif input:wasPressed("b") then
if self.onClose then self.onClose() end
end
return
end
return
self.mode = "card"
end
-- Inside a card.
local card = self:card()
@@ -1105,6 +1109,19 @@ function Pokegear:update(_dt)
-- submenu is waiting for.
local phoneBusy = card and card.id == "phone"
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
self.mode = "strip"
self:stopRadio()
@@ -2057,6 +2074,7 @@ function Pokegear:drawPlayerIcon(x, y)
local beat = math.floor((self.iconTimer or 0) / 8)
-- .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.
love.graphics.setColor(1, 1, 1, 1)
self.playerIcon:draw(x - 8, y - 8, 0, -4, "down",
beat % 2, beat == 3)
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)))
self.mon = self.party[self.index]
end
-- engine/pokemon/move_mon.asm:1402
Mon.refreshStats(self.mon, data)
self.page = opts.page or PINK_PAGE
-- ManagePokemonMoves opens straight onto MoveScreenLoop's screen; SELECT off
-- 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
self.index = 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:playCry()
return true
@@ -694,6 +698,8 @@ function SummaryMenu:switchMonPastEggs(delta)
if next_ < 1 or next_ > #self.party then return false end
self.index = 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:playCry()
return true
+30 -3
View File
@@ -45,6 +45,7 @@
local Anim = require("src.core.gen2.TradeAnim")
local Assets = require("src.render.Assets")
local Chrome = require("src.ui.gen2.Chrome")
local Font = require("src.render.Font")
local GbcPalette = require("src.render.GbcPalette")
local Music = require("src.core.Music")
local Palettes = require("src.world.gen2.Palettes")
@@ -202,6 +203,15 @@ local TEMPLATE_ROWS = {
{ 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
--------------------------------------------------------------------------
@@ -367,8 +377,22 @@ end
-- reading through.
-- `index` is a GB shade, 0 (white) to 3 (black); GbcPalette.color counts from
-- 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)
local colors = Palettes.textColors(self.palettes)
local colors = self:bgColors()
local rgb = GbcPalette.color(colors, index + 1)
or ({ { 255, 255, 255 }, { 168, 168, 168 }, { 96, 96, 96 },
{ 0, 0, 0 } })[index + 1]
@@ -384,7 +408,7 @@ end
-- and GbcPalette maps them. A driver with no shader draws the greys, which
-- is the DMG ramp and not a black frame.
function TradeAnimView:through(body)
local colors = Palettes.textColors(self.palettes)
local colors = self:bgColors()
love.graphics.setColor(1, 1, 1, 1)
if colors and GbcPalette.available() then
GbcPalette.with(colors, body)
@@ -543,7 +567,7 @@ function TradeAnimView:drawStats(record, offset)
G.setColor(1, 1, 1, 1)
G.rectangle("fill", (PANEL_X + 1) * 8, PANEL_Y * 8, 9 * 8, 8)
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
Chrome.print(Chrome.number(record.dex or 0, 3, true), PANEL_X + 7, 0)
Chrome.print(record.name, PANEL_X + 1, 2)
@@ -767,6 +791,8 @@ end
function TradeAnimView:drawPanel()
local id = (self.beat or {}).id
local t = self.offset or 0
-- engine/movie/trade_animation.asm:151
local wasBattle = Font.useBattleExtra(true)
Chrome.clear()
if GIVE_BEATS[id] then
@@ -794,6 +820,7 @@ function TradeAnimView:drawPanel()
end
end
love.graphics.setColor(1, 1, 1, 1)
Font.useBattleExtra(wasBattle)
end
function TradeAnimView:drawTubeBeat(id, t)
+34
View File
@@ -442,6 +442,27 @@ function NPC:updateTreeShake()
return true
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
-- name and meaning src/world/Collision.lua:20 gives it under Gen 1.
local function occupied(entities, tx, ty, self)
@@ -514,6 +535,10 @@ function NPC:update(map, entities)
self:updateTreeShake()
return
end
if self.rockSmash then
self:updateRockSmash()
return
end
-- 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,
-- 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.px, self.py + yOffset, 0, 0,
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
self.sprite:draw(
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.
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)
local self = setmetatable({
cellX = cx, cellY = cy,
@@ -170,9 +176,11 @@ function Player:update()
self.px = self.cellX * 16 + dx * adv
self.py = self.cellY * 16 + dy * adv
if self.jumping then
-- pokegold engine/overworld/map_objects.asm: UpdateJumpPosition's
-- y_offsets table peaks at -12.
self.py = self.py - math.floor(12 * math.sin(math.pi * self.progress / frames))
-- engine/overworld/map_objects.asm:1815
local idx = math.floor((self.progress - 1) / 2) + 1
if idx < 1 then idx = 1 end
if idx > #JUMP_Y then idx = #JUMP_Y end
self.spriteYOffset = JUMP_Y[idx]
end
if self.progress >= frames then
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.moving = false
self.jumping = nil
self.spriteYOffset = 0
self.stepFlip = not self.stepFlip
return true
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
-- lift both ride this one byte.
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
G.push()
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
-- these at the top of setMap is that refill.
blockEdits = {},
-- engine/overworld/map_setup.asm:78
objectSpawns = {},
-- A field move that is mid-flow (the used-X text, then its effect).
fieldMove = nil,
-- ---- state the script VM owns ------------------------------------------
@@ -1819,6 +1821,15 @@ function World:moveObject(objectId, cellX, cellY)
local def = self.map and self.map.def
local obj = def and def.objects and def.objects[index]
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
local npc = self:objectEntity(objectId)
if npc and npc ~= self.player then
@@ -3715,6 +3726,16 @@ function World:updateMovement()
while st.i <= #st.bytes do
local b = st.bytes[st.i]
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)
if act.kind == "end" then
-- SLIDING_F is an object flag, not a stream one, so a stream that never
@@ -5237,6 +5258,38 @@ function World:restoreBlocks()
return any
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
-- 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
@@ -5360,8 +5413,10 @@ end
-- picked, and TextBox reads it back off game.stringBuffer.
function World:setNickname(mon)
if not self.game then return end
self.game.stringBuffer =
(mon and (mon.nickname or mon.name or mon.species)) or ""
local name = (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
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
-- time the map is loaded, and this has to happen before Map.new reads them.
self:restoreBlocks()
self:restoreObjectSpawns()
-- HandleNewMap (home/map.asm:216-228) runs ResetMapBufferEventFlags before
-- anything else that touches state: event flags 0-7
-- (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).
p.inGrass, p.grassShake = false, nil
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)
return true
end