mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 12:15:31 +02:00
Merge branch 'bryanthaboi:dev' into experiment/fixed-extended-world-alignment
This commit is contained in:
+7
-1
@@ -6,6 +6,7 @@ local FixedStep = require("src.core.FixedStep")
|
||||
local Input = require("src.core.Input")
|
||||
local Logger = require("src.core.Logger")
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local GameViewport = require("src.render.GameViewport")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
local TouchControls = require("src.core.TouchControls")
|
||||
@@ -484,6 +485,7 @@ local function centerClassicZones(zones, offset)
|
||||
end
|
||||
|
||||
function Game:draw()
|
||||
GameViewport.begin(1)
|
||||
-- the UI canvas clears transparent when the overworld's world pass
|
||||
-- shows through beneath it; opaque full-screen states get the classic
|
||||
-- white clear
|
||||
@@ -597,7 +599,9 @@ function Game:draw()
|
||||
if ModRuntime.wantsHook("render.hud") then
|
||||
ModRuntime.call("render.hud", function() end, self, viewport)
|
||||
end
|
||||
-- on-screen mobile controls: pure screen-space, over the finished frame
|
||||
GameViewport.finish(self)
|
||||
-- OS-window chrome: keep the pad full-size and above any composed companion
|
||||
-- view instead of capturing and shrinking it with the game viewport.
|
||||
TouchControls:draw()
|
||||
end
|
||||
|
||||
@@ -973,8 +977,10 @@ local function pointerUnclaimed() return false end
|
||||
-- coordinates are LOVE window units, the same space render.hud's viewport
|
||||
-- and the touch overlay lay out in
|
||||
function Game:pointerEvent(phase, source, id, x, y, dx, dy, pressure, button)
|
||||
local gameX, gameY, insideGame = GameViewport.toLocal(x, y)
|
||||
return ModRuntime.call("input.pointer", pointerUnclaimed, self, {
|
||||
phase = phase, source = source, id = id, x = x, y = y,
|
||||
gameX = gameX, gameY = gameY, insideGame = insideGame,
|
||||
dx = dx or 0, dy = dy or 0, pressure = pressure, button = button,
|
||||
})
|
||||
end
|
||||
|
||||
+33
-20
@@ -35,6 +35,7 @@ local World = require("src.world.gen2.World")
|
||||
-- The mod event/hook buses. Gold reaches them through Runtime like every
|
||||
-- other engine file, so a call site here is the same call site Gen 1 has.
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
local GameViewport = require("src.render.GameViewport")
|
||||
-- Only for the mod-supplied save migrations and the mods-changed report, which
|
||||
-- are keyed off save.meta and know nothing about a generation; Gold's own save
|
||||
-- IO is src/core/gen2/Save.lua.
|
||||
@@ -73,8 +74,8 @@ end
|
||||
--
|
||||
-- Gold composites its own frame (Game2:draw / drawScene) and pumps its own pad
|
||||
-- (the FixedStep callback in Game2:load), so none of it goes through
|
||||
-- src/render/Renderer.lua or src/core/Game.lua. That explains why the eight
|
||||
-- hooks below never used to fire here; it is not a reason they should not. A
|
||||
-- src/render/Renderer.lua or src/core/Game.lua. That explains why the hooks
|
||||
-- below never used to fire here; it is not a reason they should not. A
|
||||
-- hook is a contract about a MOMENT in the frame, and Gold has every one of
|
||||
-- these moments -- so each is raised under the Gen 1 NAME with the Gen 1
|
||||
-- PAYLOAD, at the Gen 1 point in the order:
|
||||
@@ -86,6 +87,8 @@ end
|
||||
-- render.output* the normal composed frame (Renderer.lua:1063)
|
||||
-- render.letterbox the void around the 160x144 blit (Renderer.lua:840)
|
||||
-- render.hud screen-space UI over the frame (src/core/Game.lua:521)
|
||||
-- render.viewport the game's OS-window rectangle (GameViewport.lua:52)
|
||||
-- render.window final OS-window composition (GameViewport.lua:145)
|
||||
--
|
||||
-- Where Gold genuinely cannot tell two Gen 1 things apart -- it composites the
|
||||
-- world pass and the UI into ONE canvas, not two -- the call site says so and
|
||||
@@ -730,7 +733,7 @@ function Game2:usePartyItem(itemId)
|
||||
local row = ItemEffects.RESTORE_PP[itemId] or {}
|
||||
if row.each or mon.isEgg then
|
||||
self.stack:pop()
|
||||
finish(ItemEffects.usePpItem(itemId, mon), mon)
|
||||
finish(ItemEffects.usePpItem(itemId, mon, nil, self.data), mon)
|
||||
return
|
||||
end
|
||||
Screens.push(self, "Gen2MoveDeleter", {
|
||||
@@ -740,7 +743,7 @@ function Game2:usePartyItem(itemId)
|
||||
onChoose = function(slot)
|
||||
self.stack:pop() -- the move list
|
||||
self.stack:pop() -- the party list
|
||||
finish(ItemEffects.usePpItem(itemId, mon, slot), mon)
|
||||
finish(ItemEffects.usePpItem(itemId, mon, slot, self.data), mon)
|
||||
end,
|
||||
})
|
||||
end,
|
||||
@@ -888,6 +891,12 @@ function Game2:load()
|
||||
self.data.items = loadGenerated("data/generated/items.lua") or {}
|
||||
self.data.moves = loadGenerated("data/generated/moves.lua") or {}
|
||||
self.data.type_chart = loadGenerated("data/generated/type_chart.lua") or {}
|
||||
-- data/types/type_matchups.asm:112-116: the rows after the `db -2` marker
|
||||
-- apply by default; Foresight is what cuts the table short at it.
|
||||
local chart = self.data.type_chart
|
||||
for _, row in ipairs(chart.foresightMatchups or {}) do
|
||||
chart.matchups[#chart.matchups + 1] = row
|
||||
end
|
||||
-- The `held_items` registry's merge target: ItemAttributes' last two columns
|
||||
-- as their own table, so a mod can give an item a held behaviour without
|
||||
-- owning the whole item record. Built BEFORE mods:load so the registry
|
||||
@@ -1191,9 +1200,7 @@ function Game2:frameFit(w, h)
|
||||
dpi = tonumber(love.window.getDPIScale()) or 1
|
||||
end
|
||||
local pw, ph = w * dpi, h * dpi
|
||||
if love.graphics.getPixelDimensions then
|
||||
pw, ph = love.graphics.getPixelDimensions()
|
||||
end
|
||||
pw, ph = GameViewport.pixelDimensions()
|
||||
return scale, ox, oy, dpi, pw, ph
|
||||
end
|
||||
|
||||
@@ -1211,18 +1218,15 @@ function Game2:viewport(w, h)
|
||||
}
|
||||
end
|
||||
|
||||
-- The screen-space layer, in the Gen 1 order: render.hud and then the
|
||||
-- on-screen pad (src/core/Game.lua:521 and :524, either side of
|
||||
-- Renderer:endFrame). Both are window-space, both sit over the finished
|
||||
-- frame -- post passes, letterbox and all -- and neither ever enters the game
|
||||
-- canvas. Every exit path of Game2:draw ends here, which is what makes that
|
||||
-- true of the composed frame a mod owns as well as of the plain one.
|
||||
-- The render.hud layer, in Gen 1's order over the finished game frame. The
|
||||
-- on-screen pad is drawn separately after GameViewport.finish, because it is
|
||||
-- OS-window chrome and must not be captured or scaled with this canvas.
|
||||
--
|
||||
-- render.hud: persistent tool status. The call is fenced with
|
||||
-- push("all")/pop for the reason src/render/Pipelines.lua:guardRender fences a
|
||||
-- mod render callback: a subscriber that returns cleanly but leaves a shader
|
||||
-- bound, the canvas redirected or the colour changed must not corrupt the next
|
||||
-- frame -- or, now, the pad drawn immediately after it.
|
||||
-- frame.
|
||||
function Game2:drawHud(w, h)
|
||||
if ModRuntime.wantsHook("render.hud") then
|
||||
local G = love.graphics
|
||||
@@ -1230,10 +1234,6 @@ function Game2:drawHud(w, h)
|
||||
ModRuntime.call("render.hud", noop, self, self:viewport(w, h))
|
||||
G.pop()
|
||||
end
|
||||
-- The pad LAST, so a HUD mod cannot draw over the controls the player is
|
||||
-- pressing. It draws nothing at all off Android/iOS unless POKEPORT_TOUCH=1
|
||||
-- forces it, and nothing ever while a controller is in use.
|
||||
TouchControls:draw()
|
||||
end
|
||||
|
||||
-- render.letterbox: SGB borders and custom void art in the bars around the
|
||||
@@ -1357,9 +1357,9 @@ end
|
||||
-- is being shown on. Mod post-processes fold in between the two, where
|
||||
-- Renderer.lua:1058 folds them -- a blur or a colour grade is what the LCD grid
|
||||
-- is then drawn over, rather than something that smears the grid itself.
|
||||
function Game2:draw()
|
||||
function Game2:drawViewportFrame()
|
||||
local G = love.graphics
|
||||
local w, h = G.getDimensions()
|
||||
local w, h = GameViewport.dimensions()
|
||||
local GBCFX = require("src.render.GBCFX")
|
||||
local GbcPalette = require("src.render.GbcPalette")
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
@@ -1471,6 +1471,16 @@ function Game2:draw()
|
||||
self:drawHud(w, h)
|
||||
end
|
||||
|
||||
function Game2:draw()
|
||||
GameViewport.begin(2)
|
||||
GameViewport.setTarget()
|
||||
self:drawViewportFrame()
|
||||
GameViewport.finish(self)
|
||||
-- OS-window chrome: draw after companion composition so viewport layouts
|
||||
-- neither shrink nor cover the touch pad.
|
||||
TouchControls:draw()
|
||||
end
|
||||
|
||||
-- The paper a pushed TextBox has to sit on. A textbox is built entirely from
|
||||
-- font-page tiles ($79-$7e frame, ' ' $7f interior), so it takes BG palette 0
|
||||
-- colour 0 from the screen UNDER it (pokegold engine/pokegear/pokegear.asm
|
||||
@@ -1797,8 +1807,10 @@ end
|
||||
|
||||
-- coordinates are LOVE window units, the same space render.hud's viewport is in
|
||||
function Game2:pointerEvent(phase, source, id, x, y, dx, dy, pressure, button)
|
||||
local gameX, gameY, insideGame = GameViewport.toLocal(x, y)
|
||||
return ModRuntime.call("input.pointer", pointerUnclaimed, self, {
|
||||
phase = phase, source = source, id = id, x = x, y = y,
|
||||
gameX = gameX, gameY = gameY, insideGame = insideGame,
|
||||
dx = dx or 0, dy = dy or 0, pressure = pressure, button = button,
|
||||
})
|
||||
end
|
||||
@@ -1949,6 +1961,7 @@ function Game2:applyOptions()
|
||||
touchControls = options.touchControls,
|
||||
haptics = options.haptics,
|
||||
})
|
||||
require("src.core.VideoMode").applyOptions(options)
|
||||
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.
|
||||
|
||||
+74
-10
@@ -272,6 +272,38 @@ function HostShell.quote(s)
|
||||
return "'" .. s:gsub("'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
-- Launch another instance of this packaged app without waiting for it. The
|
||||
-- same path works on all process-capable desktop hosts; only the shell's
|
||||
-- background spelling differs. Source checkouts include their game folder,
|
||||
-- while fused releases and AppImages already carry it in the executable.
|
||||
function HostShell.spawnSelfDetached(args)
|
||||
if not require("src.core.Platform").canSpawnProcess() then return false end
|
||||
local fs = love and love.filesystem
|
||||
if not (fs and fs.getExecutablePath) then return false end
|
||||
local executable = os.getenv("APPIMAGE") or fs.getExecutablePath()
|
||||
if type(executable) ~= "string" or executable == "" then return false end
|
||||
|
||||
local argv = {}
|
||||
local fused = fs.isFused and fs.isFused()
|
||||
if not os.getenv("APPIMAGE") and not fused and fs.getSource then
|
||||
argv[#argv + 1] = fs.getSource()
|
||||
end
|
||||
for _, value in ipairs(args or {}) do argv[#argv + 1] = tostring(value) end
|
||||
|
||||
local command = HostShell.quote(executable)
|
||||
for _, value in ipairs(argv) do
|
||||
command = command .. " " .. HostShell.quote(value)
|
||||
end
|
||||
local osName = love.system and love.system.getOS and love.system.getOS()
|
||||
if osName == "Windows" then
|
||||
command = 'start "" /b ' .. command .. " >NUL 2>&1"
|
||||
else
|
||||
command = HostShell.envPrefix() .. command .. " >/dev/null 2>&1 &"
|
||||
end
|
||||
local ok, _, code = os.execute(command)
|
||||
return ok == true or ok == 0 or code == 0
|
||||
end
|
||||
|
||||
-- MEMOISED per Lua state (so once per thread). This used to spawn a whole
|
||||
-- `curl --version` process on every single fetch -- twice for a GET through
|
||||
-- the Android-bridge fallback -- which doubled the number of spawns the lock
|
||||
@@ -428,10 +460,43 @@ function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
||||
if type(body) ~= "string" then return nil, "missing body" end
|
||||
userAgent = userAgent or "gen1recomp"
|
||||
if HostShell.haveCurl() then
|
||||
-- --data-binary @- keeps the payload out of argv (command-line length
|
||||
-- io.popen is one-way on Lua/LuaJIT: its mode is "r" or "w", never
|
||||
-- "rw". Stage the request body so the response can stay on a read
|
||||
-- pipe. The staging directory comes from the OS temp contract, never
|
||||
-- tmpnam(): the CRT's tmpnam() can return a name relative to the process
|
||||
-- working directory, and a game installed under Program Files has no
|
||||
-- writable CWD -- io.open would fail before curl ever runs and postLog
|
||||
-- would silently drop the send. TEMP/TMP are per-user writable on
|
||||
-- Windows; TMPDIR (with /tmp fallback) covers POSIX. No love.filesystem:
|
||||
-- the sandbox-era transport stays on plain io/os.
|
||||
local function stagingPath()
|
||||
local dir = os.getenv("TEMP") or os.getenv("TMP")
|
||||
if not dir or dir == "" then dir = os.getenv("TMPDIR") or "/tmp" end
|
||||
local sep = dir:find("\\") and "\\" or "/"
|
||||
return dir .. sep .. ("gen1recomp-post-%d.tmp"):format(
|
||||
(os.time() % 1000000) * 100 + math.random(0, 99))
|
||||
end
|
||||
local bodyPath = stagingPath()
|
||||
local bodyFile, bodyOpenErr = io.open(bodyPath, "wb")
|
||||
if not bodyFile then
|
||||
pcall(os.remove, bodyPath)
|
||||
return nil, "could not create request body: " .. tostring(bodyOpenErr)
|
||||
end
|
||||
local bodyOk, bodyErr = pcall(function()
|
||||
assert(bodyFile:write(body))
|
||||
assert(bodyFile:close())
|
||||
end)
|
||||
if not bodyOk then
|
||||
pcall(function() bodyFile:close() end)
|
||||
pcall(os.remove, bodyPath)
|
||||
return nil, "could not write body: " .. tostring(bodyErr)
|
||||
end
|
||||
|
||||
-- --data-binary @<file> keeps the payload out of argv (command-line length
|
||||
-- limits on Windows) and preserves every byte including trailing
|
||||
-- newlines. No -f, matching httpGet: the response body is discarded
|
||||
-- anyway, and curl's stderr carries the real diagnosis on failure.
|
||||
-- newlines. The body is staged above because io.popen cannot be opened
|
||||
-- for both writing and reading. No -f, matching httpGet: the response
|
||||
-- body is discarded anyway, and curl's stderr carries the diagnosis.
|
||||
local cmd = ("curl -sSL --proto =http,https --proto-redir =http,https "
|
||||
.. "--connect-timeout 10 --max-time %d ")
|
||||
:format(tonumber(maxTime) or 40)
|
||||
@@ -441,18 +506,17 @@ function HostShell.httpPost(url, body, contentType, userAgent, maxTime)
|
||||
cmd = cmd .. "-H " .. HostShell.quote("Content-Type: " .. contentType) .. " "
|
||||
end
|
||||
cmd = cmd .. "-H " .. HostShell.quote("Content-Length: " .. tostring(#body)) .. " "
|
||||
.. "--data-binary @- "
|
||||
.. "--data-binary " .. HostShell.quote("@" .. bodyPath) .. " "
|
||||
.. "-w " .. HostShell.quote(HTTP_MARK_FMT) .. " "
|
||||
.. HostShell.quote(url) .. " 2>&1"
|
||||
local pipe = HostShell.popen(cmd, "rw")
|
||||
if not pipe then return nil, "could not run curl" end
|
||||
local writeOk, werr = pcall(pipe.write, pipe, body)
|
||||
if not writeOk then
|
||||
HostShell.pclose(pipe)
|
||||
return nil, "could not write body: " .. tostring(werr)
|
||||
local pipe = HostShell.popen(cmd)
|
||||
if not pipe then
|
||||
pcall(os.remove, bodyPath)
|
||||
return nil, "could not run curl"
|
||||
end
|
||||
local readOk, out = pcall(function() return pipe:read("*a") end)
|
||||
HostShell.pclose(pipe)
|
||||
pcall(os.remove, bodyPath)
|
||||
if not readOk then
|
||||
return nil, fetchError(url, nil, tostring(out))
|
||||
end
|
||||
|
||||
+41
-10
@@ -130,12 +130,23 @@ local SPECIAL = {
|
||||
evolution = "Music_SafariZone",
|
||||
}
|
||||
|
||||
-- SpecialMapMusic (pokegold home/audio.asm:397)
|
||||
local SPECIAL_GEN2 = {
|
||||
surf = "Music_Surf",
|
||||
bike = "Music_Bicycle",
|
||||
evolution = "Music_Evolution",
|
||||
}
|
||||
|
||||
-- the label a scene role resolves to; call sites keep their own presence
|
||||
-- guard on the resolved label
|
||||
function Music.special(data, key)
|
||||
local special = data and data.audio and data.audio.special
|
||||
local label = special and special[key]
|
||||
if label ~= nil then return label end
|
||||
if data and data.audio and data.audio.generation == 2
|
||||
and SPECIAL_GEN2[key] ~= nil then
|
||||
return SPECIAL_GEN2[key]
|
||||
end
|
||||
return SPECIAL[key]
|
||||
end
|
||||
|
||||
@@ -192,6 +203,12 @@ local function startSong(data, def, wantLoop)
|
||||
return nil, nil, nil, "no chip program and no file"
|
||||
end
|
||||
|
||||
-- Music_MeetRival_Ch{1,2,3}_AlternateStart (audio/alternate_tempo.asm:7)
|
||||
local RIVAL_ALT_START = {
|
||||
redblue = { 0x71a2, 0x721d, 0x72b5 },
|
||||
yellow = { 0x7075, 0x70f0, 0x7188 },
|
||||
}
|
||||
|
||||
-- the single choke point every song choice passes through, so one hook
|
||||
-- covers map themes, battle themes, jingles and scene music
|
||||
local function selectSong(song, ctx)
|
||||
@@ -219,8 +236,17 @@ function Music.play(data, song, loop, ctx)
|
||||
local start = ctx and ctx.start or nil
|
||||
-- a hook may silence the cue outright, or swap in a label the dedupe
|
||||
-- below has to compare against
|
||||
if not song or (song == state.current and tempo == state.tempo
|
||||
and start == state.start) then return end
|
||||
if not song then return end
|
||||
if song == state.current and tempo == state.tempo
|
||||
and start == state.start then
|
||||
-- ..(home/audio.asm ln 65)
|
||||
if state.fade and state.fade.pending then
|
||||
state.fade = nil
|
||||
applyVolume(state.source)
|
||||
applyVolume(state.loopSource)
|
||||
end
|
||||
return
|
||||
end
|
||||
local def = songDef(data, song)
|
||||
if not def or state.failed[song] then return end
|
||||
|
||||
@@ -247,10 +273,12 @@ function Music.play(data, song, loop, ctx)
|
||||
and def.bank == 2 and def.address == 17050 then
|
||||
local started = {}
|
||||
for key, value in pairs(def) do started[key] = value end
|
||||
local alt = require("src.core.GameVersion").isYellow()
|
||||
and RIVAL_ALT_START.yellow or RIVAL_ALT_START.redblue
|
||||
started.startChannels = {
|
||||
{ number = 1, address = 0x71a2 },
|
||||
{ number = 2, address = 0x721d },
|
||||
{ number = 3, address = 0x72b5 },
|
||||
{ number = 1, address = alt[1] },
|
||||
{ number = 2, address = alt[2] },
|
||||
{ number = 3, address = alt[3] },
|
||||
}
|
||||
def = started
|
||||
end
|
||||
@@ -340,9 +368,12 @@ end
|
||||
|
||||
Music.MAP_FADE = 10
|
||||
|
||||
-- the song a map should currently play, honoring the bike/surf overrides
|
||||
-- the song a map should currently play, honoring the bike/surf overrides;
|
||||
-- Gen 2 has no outdoor gate (pokegold home/audio.asm:437)
|
||||
local function effectiveMapSong(data, song)
|
||||
if not song or not outdoorSongs(data)[song] then return song end
|
||||
if not song then return song end
|
||||
local gen2 = data and data.audio and data.audio.generation == 2
|
||||
if not gen2 and not outdoorSongs(data)[song] then return song end
|
||||
if state.onBike then
|
||||
local bike = Music.special(data, "bike")
|
||||
if bike and songDef(data, bike) then return bike end
|
||||
@@ -356,9 +387,9 @@ end
|
||||
|
||||
-- overworld map theme; onBike/surfing override outdoor themes with the
|
||||
-- bike/surf songs and restore the map theme when they end
|
||||
function Music.playMap(data, mapId, onBike, surfing, fade)
|
||||
local song = data and data.audio and data.audio.mapSongs
|
||||
and mapId and data.audio.mapSongs[mapId] or nil
|
||||
function Music.playMap(data, mapId, onBike, surfing, fade, song)
|
||||
song = song or (data and data.audio and data.audio.mapSongs
|
||||
and mapId and data.audio.mapSongs[mapId]) or nil
|
||||
state.mapSong = song
|
||||
state.onBike = not not onBike
|
||||
state.surfing = not not surfing
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
-- (touch overlay, launcher) should prefer this over getDimensions; the game
|
||||
-- canvas may still letterbox into the full framebuffer for immersion.
|
||||
|
||||
local GameViewport = require("src.render.GameViewport")
|
||||
|
||||
local SafeArea = {}
|
||||
|
||||
function SafeArea.rect()
|
||||
function SafeArea.windowRect()
|
||||
local ww, wh = 0, 0
|
||||
if love and love.graphics and love.graphics.getDimensions then
|
||||
ww, wh = love.graphics.getDimensions()
|
||||
ww, wh = GameViewport.fullDimensions()
|
||||
end
|
||||
if ww <= 0 then ww = 1 end
|
||||
if wh <= 0 then wh = 1 end
|
||||
@@ -55,4 +57,8 @@ function SafeArea.rect()
|
||||
return x, y, w, h
|
||||
end
|
||||
|
||||
function SafeArea.rect()
|
||||
return GameViewport.localSafeRect(SafeArea.windowRect())
|
||||
end
|
||||
|
||||
return SafeArea
|
||||
|
||||
@@ -339,7 +339,7 @@ end
|
||||
-- demand. Mirrors it into self.orientation / self.positions / self.scale,
|
||||
-- which layout(), the editor chrome and the tests read.
|
||||
function TouchControls:currentBucket()
|
||||
local _, _, sw, sh = SafeArea.rect()
|
||||
local _, _, sw, sh = SafeArea.windowRect()
|
||||
local o = orientationFor(sw, sh)
|
||||
self.layouts = self.layouts or { portrait = {}, landscape = {} }
|
||||
local b = self.layouts[o]
|
||||
@@ -363,7 +363,7 @@ end
|
||||
-- while sizes stay derived from the short edge, times the orientation's
|
||||
-- size setting (#633).
|
||||
function TouchControls:layout()
|
||||
local ox, oy, sw, sh = SafeArea.rect()
|
||||
local ox, oy, sw, sh = SafeArea.windowRect()
|
||||
if self.layoutW == sw and self.layoutH == sh
|
||||
and self.layoutOx == ox and self.layoutOy == oy and self.L then
|
||||
return self.L
|
||||
@@ -397,7 +397,7 @@ end
|
||||
-- Move one control to a screen-space point and persist its normalized
|
||||
-- position within the safe rect. Used by the layout editor while dragging.
|
||||
function TouchControls:setControlCenter(name, cx, cy)
|
||||
local ox, oy, sw, sh = SafeArea.rect()
|
||||
local ox, oy, sw, sh = SafeArea.windowRect()
|
||||
local L = self:layout()
|
||||
local zone = L[name]
|
||||
if not zone then return end
|
||||
@@ -608,10 +608,10 @@ local function drawIcon(img, zone, pressed, alphaMul)
|
||||
zone.cy - img:getHeight() * scale / 2, 0, scale, scale)
|
||||
end
|
||||
|
||||
-- Screen-space, called by Game:draw after Renderer:endFrame -- and by
|
||||
-- Game2:drawHud after Gold's own present pass -- so the overlay rides on top
|
||||
-- of everything (world, UI, CRT/GBC FX included). Also used by the launcher
|
||||
-- layout editor under preview mode.
|
||||
-- OS-window space, called after GameViewport.finish so the overlay rides on
|
||||
-- top of the game, companion composition and post-processing without being
|
||||
-- captured or scaled with any game viewport. Also used by the launcher layout
|
||||
-- editor under preview mode.
|
||||
function TouchControls:draw()
|
||||
if not self:visible() then return end
|
||||
local L = self:layout()
|
||||
|
||||
@@ -58,6 +58,12 @@ ItemEffects.RESTORE_PP = {
|
||||
MAX_ELIXER = { amount = "all", each = true },
|
||||
}
|
||||
|
||||
-- engine/items/item_effects.asm:1245 StatExpItemPointerOffsets.
|
||||
ItemEffects.VITAMIN = {
|
||||
HP_UP = "hp", PROTEIN = "attack", IRON = "defense",
|
||||
CARBOS = "speed", CALCIUM = "special",
|
||||
}
|
||||
|
||||
-- EnergypowderEnergyRootCommon / HealPowderEffect: the herb items charge
|
||||
-- happiness for tasting bitter on top of their heal.
|
||||
local BITTER = {
|
||||
@@ -70,6 +76,9 @@ ItemEffects.TEXT_NO_EFFECT = "It won't have any\neffect."
|
||||
ItemEffects.TEXT_CANT_USE_ON_EGG = "That can't be used\non an EGG."
|
||||
-- _PPRestoredText (data/text/common_3.asm).
|
||||
ItemEffects.TEXT_PP_RESTORED = "PP was restored."
|
||||
-- _PPIsMaxedOutText / _PPsIncreasedText (data/text/common_3.asm).
|
||||
ItemEffects.TEXT_PP_MAXED = "%s's PP\nis maxed out."
|
||||
ItemEffects.TEXT_PP_INCREASED = "%s's PP\nincreased."
|
||||
|
||||
-- PrintPartyMenuActionText's .MenuActionTexts (engine/pokemon/party_menu.asm),
|
||||
-- keyed by the class GetItemHealingAction resolves. Each is the two rows the
|
||||
@@ -235,6 +244,33 @@ local function rareCandy(mon, data)
|
||||
}
|
||||
end
|
||||
|
||||
-- engine/items/item_effects.asm:1216 StatStrings.
|
||||
local VITAMIN_LABEL = {
|
||||
hp = "HEALTH", attack = "ATTACK", defense = "DEFENSE",
|
||||
speed = "SPEED", special = "SPECIAL",
|
||||
}
|
||||
|
||||
-- engine/items/item_effects.asm:1149 VitaminEffect.
|
||||
local function vitamin(itemId, mon, data)
|
||||
local stat = ItemEffects.VITAMIN[itemId]
|
||||
mon.statExp = mon.statExp or Mon.newStatExp()
|
||||
local cur = mon.statExp[stat] or 0
|
||||
if cur >= 25600 then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
end
|
||||
mon.statExp[stat] = math.min(Mon.MAX_STAT_EXP, cur + 2560)
|
||||
local def = data and data.pokemon and data.pokemon[mon.species]
|
||||
if def and def.baseStats then
|
||||
mon.stats = Mon.stats(def.baseStats, mon.dvs, mon.level, mon.statExp)
|
||||
mon.maxHp = mon.stats.hp
|
||||
end
|
||||
Happiness.change(mon, "USEDITEM")
|
||||
return {
|
||||
used = true,
|
||||
text = ("%s's\n%s rose."):format(monName(mon), VITAMIN_LABEL[stat]),
|
||||
}
|
||||
end
|
||||
|
||||
-- --------------------------------------------------------- held attributes
|
||||
--
|
||||
-- The `held_items` registry (src/mods/Schemas.lua), which is the last two
|
||||
@@ -341,11 +377,8 @@ function ItemEffects.recordFor(itemId, data)
|
||||
return (merged and merged[itemId]) or ItemEffects.RECORDS[itemId]
|
||||
end
|
||||
|
||||
-- Which family a PACK item runs on a party mon, or nil for anything whose
|
||||
-- ITEMMENU_PARTY behaviour is not ported (vitamins, PP UP, evolution stones).
|
||||
-- FULL_RESTORE classifies as "heal"; its full-HP status arm lives inside the
|
||||
-- record's own `use` the way FullRestoreEffect keeps both halves in one
|
||||
-- routine.
|
||||
-- Which family a PACK item runs on a party mon, or nil for an id with no
|
||||
-- item_effects record (engine/items/pack.asm UseItem, ITEMMENU_PARTY arm).
|
||||
function ItemEffects.partyAction(itemId, data)
|
||||
local record = ItemEffects.recordFor(itemId, data)
|
||||
return record and record.action or nil
|
||||
@@ -446,6 +479,36 @@ for itemId, row in pairs(ItemEffects.RESTORE_PP) do
|
||||
end)
|
||||
end
|
||||
|
||||
-- engine/items/item_effects.asm:2320 RestorePPEffect's PP_UP arm.
|
||||
record("PP_UP", "pp", function(ctx)
|
||||
local move = (ctx.mon.moves or {})[ctx.slot]
|
||||
if type(move) ~= "table" or not move.id then
|
||||
return { used = false, text = ItemEffects.TEXT_NO_EFFECT }
|
||||
end
|
||||
local row = ((ctx.data and ctx.data.moves) or {})[move.id]
|
||||
local name = (row and row.name) or move.id
|
||||
-- constants/pokemon_data_constants.asm:216 PP_UP_MASK.
|
||||
if move.id == "SKETCH" or (move.ppUps or 0) >= 3 then
|
||||
return { used = false, text = ItemEffects.TEXT_PP_MAXED:format(name) }
|
||||
end
|
||||
local base = (row and row.pp) or move.maxPp
|
||||
if not base then
|
||||
return { used = false, text = ItemEffects.TEXT_PP_MAXED:format(name) }
|
||||
end
|
||||
-- engine/items/item_effects.asm:2736 ComputeMaxPP.
|
||||
local bonus = math.min(math.floor(base / 5), 7)
|
||||
move.ppUps = (move.ppUps or 0) + 1
|
||||
move.maxPp = base + move.ppUps * bonus
|
||||
move.pp = (move.pp or 0) + bonus
|
||||
return { used = true, text = ItemEffects.TEXT_PP_INCREASED:format(name) }
|
||||
end)
|
||||
|
||||
for itemId in pairs(ItemEffects.VITAMIN) do
|
||||
record(itemId, "vitamin", function(ctx)
|
||||
return vitamin(ctx.item, ctx.mon, ctx.data)
|
||||
end)
|
||||
end
|
||||
|
||||
record("RARE_CANDY", "candy", function(ctx)
|
||||
return rareCandy(ctx.mon, ctx.data)
|
||||
end)
|
||||
|
||||
@@ -272,6 +272,7 @@ Save.DEFAULT_OPTIONS = {
|
||||
-- Game Boy. The Gen 1 save's equivalent key is `colors` (SGB packs), which
|
||||
-- means something different, hence the different name.
|
||||
color = "gbc",
|
||||
videoMode = "windowed",
|
||||
musicVol = 7, -- 0-7, like the GB's NR50 master volume
|
||||
sfxVol = 7, -- 0-7
|
||||
musicFilter = 0, -- low-pass steps, 0 = off
|
||||
|
||||
Reference in New Issue
Block a user