Merge branch 'bryanthaboi:dev' into experiment/fixed-extended-world-alignment

This commit is contained in:
James Hall
2026-08-16 19:30:19 -05:00
committed by GitHub
233 changed files with 8377 additions and 45512 deletions
+7 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+8 -2
View File
@@ -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
+7 -7
View File
@@ -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()
+68 -5
View File
@@ -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)
+1
View File
@@ -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