mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
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:
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user