mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
CLOSES #1181, CLOSES #1212, CLOSES #1214, CLOSES #1224, CLOSES #1230, CLOSES #1249, CLOSES #1271, CLOSES #1272, CLOSES #1273, CLOSES #1298, CLOSES #1305, CLOSES #1307, CLOSES #1318, CLOSES #1328, CLOSES #1330, CLOSES #1331, CLOSES #1333, CLOSES #1334, CLOSES #1335, CLOSES #1340, CLOSES #1345, CLOSES #1346, CLOSES #1360, CLOSES #1362 (#1395)
* CLOSES #1181, CLOSES #1212, CLOSES #1214, CLOSES #1224, CLOSES #1230, CLOSES #1249, CLOSES #1271, CLOSES #1272, CLOSES #1273, CLOSES #1298, CLOSES #1305, CLOSES #1307, CLOSES #1318, CLOSES #1328, CLOSES #1330, CLOSES #1331, CLOSES #1333, CLOSES #1334, CLOSES #1335, CLOSES #1340, CLOSES #1345, CLOSES #1346, CLOSES #1360, CLOSES #1362 * conv
This commit is contained in:
+45
-51
@@ -372,21 +372,17 @@ local function grayImage(img)
|
||||
return getImage(meta.path) or img
|
||||
end
|
||||
|
||||
-- The blacked-out battle screen. HandlePlayerBlackOut (core.asm:1151) runs
|
||||
-- SET_PAL_BATTLE_BLACK, i.e. SetPal_BattleBlack sends PalPacket_Black --
|
||||
-- PAL_BLACK in all four slots of BlkPacket_Battle (engine/gfx/palettes.asm:
|
||||
-- 22-25). The mon pics are drawn OVER the zone pass with their palette
|
||||
-- already baked in, so darkening them means re-baking through PAL_BLACK the
|
||||
-- way fadeImage re-bakes through a BGP permutation (#292). Reads the palette
|
||||
-- out of the active pack, exactly like sgbBattlePals, so the zone pass and
|
||||
-- the pics can never disagree. trueColor art has no DMG shades to remap.
|
||||
-- SET_PAL_BATTLE_BLACK re-bake of a pic (engine/battle/core.asm:1151,
|
||||
-- engine/gfx/palettes.asm:22-25); resolved like sgbBattlePals' blackout (#292).
|
||||
local function blackImage(data, img)
|
||||
local meta = imageMeta[img]
|
||||
if not meta or meta.trueColor then return img end
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local pack = PaletteFX.pack(data)
|
||||
local colors = pack and pack.palettes and pack.palettes.BLACK
|
||||
if not colors then return img end
|
||||
local pals = pack and pack.palettes
|
||||
if not (pals and pals.BLACK) then return img end
|
||||
local colors = PaletteFX.usesYellowCgb() and pals.BLACK
|
||||
or PaletteFX.pal(data, "BLACK") or pals.BLACK
|
||||
local name = PaletteFX.usesGbcPack() and "redpp:BLACK" or "BLACK"
|
||||
return getImage(meta.path, { name = name, colors = colors }) or img
|
||||
end
|
||||
@@ -2651,6 +2647,9 @@ function BattleState:residualFor(b, opp)
|
||||
if self.result then return end
|
||||
if self.player ~= b and self.enemy ~= b then return end
|
||||
if b.mon.hp <= 0 or opp.mon.hp <= 0 then return end
|
||||
-- engine/battle/core.asm:435-473
|
||||
if b.residualDone then return end
|
||||
b.residualDone = true
|
||||
local msgs = Status.residual(b, opp, self)
|
||||
for _, m in ipairs(msgs) do self:sayNext(prefixEnemy(m, b)) end
|
||||
if b.leechSeeded and b.mon.hp > 0 then
|
||||
@@ -2707,7 +2706,8 @@ function BattleState:endOfTurn()
|
||||
for _, pair in ipairs({ { self.player, self.enemy, "player", enemyAlive },
|
||||
{ self.enemy, self.player, "enemy", playerAlive } }) do
|
||||
local b, opp, side, oppAlive = pair[1], pair[2], pair[3], pair[4]
|
||||
if sweep and b.mon.hp > 0 and oppAlive then
|
||||
if sweep and not b.residualDone and b.mon.hp > 0 and oppAlive then
|
||||
b.residualDone = true
|
||||
local msgs = Status.residual(b, opp, self)
|
||||
for _, m in ipairs(msgs) do self:sayNext(prefixEnemy(m, b)) end
|
||||
if #msgs > 0 then self:drainNext() end -- poison/burn/seed HP moved
|
||||
@@ -2721,6 +2721,7 @@ function BattleState:endOfTurn()
|
||||
-- the Haze move-forfeit only covers the turn Haze was used; if the
|
||||
-- cured mon had already moved, drop the flag before next turn
|
||||
b.skipMove = nil
|
||||
b.residualDone = nil
|
||||
-- CheckNumAttacksLeft (core.asm:683-697): a trapping counter that
|
||||
-- hit 0 this turn releases its bit only now, at the end of the turn
|
||||
if b.trappingTurns and b.trappingTurns <= 0 then
|
||||
@@ -3935,9 +3936,9 @@ function BattleState:onFaint(battler)
|
||||
battler.fainted = true
|
||||
local Sound = require("src.core.Sound")
|
||||
if battler.isPlayer then
|
||||
-- RemoveFaintedPlayerMon (core.asm:1040-1042): the player mon's
|
||||
-- faint plays its ordinary species cry -- no Faint_Fall
|
||||
self.faintCry = Sound.playCry(self.data, battler.mon.species)
|
||||
-- RemoveFaintedPlayerMon: the species cry (core.asm:1040-1042),
|
||||
-- PikachuCry4 on Yellow (engine/battle/core.asm:1058)
|
||||
self.faintCry = Sound.playCry(self.data, battler.mon.species, 4)
|
||||
elseif self.kind ~= "wild" then
|
||||
-- FaintEnemyPokemon (core.asm:732-771): the enemy faint plays no
|
||||
-- species cry; trainer battles get SFX_FAINT_FALL, then SFX_FAINT_THUD
|
||||
@@ -5339,24 +5340,28 @@ function BattleState:sgbBattlePals()
|
||||
local pack = PaletteFX.pack(self.data)
|
||||
local pals = pack and pack.palettes
|
||||
if not pals then return nil end
|
||||
-- HandlePlayerBlackOut (core.asm:1151) runs SET_PAL_BATTLE_BLACK:
|
||||
-- SetPal_BattleBlack sends PalPacket_Black, PAL_BLACK in all four slots of
|
||||
-- BlkPacket_Battle (engine/gfx/palettes.asm:22-25), so every zone of the
|
||||
-- battle screen -- both HP bars and both mon regions -- goes dark behind
|
||||
-- the blackout text. picImage re-bakes the pics through the same palette,
|
||||
-- since those draw over the zone pass rather than through it (#292).
|
||||
-- HandlePlayerBlackOut (core.asm:1151) runs SET_PAL_BATTLE_BLACK
|
||||
-- (engine/gfx/palettes.asm:22-25); picImage re-bakes through it (#292).
|
||||
if self.blackedOut and pals.BLACK then
|
||||
local b = pals.BLACK
|
||||
local b = PaletteFX.usesYellowCgb() and pals.BLACK
|
||||
or PaletteFX.pal(self.data, "BLACK") or pals.BLACK
|
||||
return { [0] = b, [1] = b, [2] = b, [3] = b }
|
||||
end
|
||||
-- home/palettes.asm:38
|
||||
local function bar(b)
|
||||
if not b then return pals.GREENBAR end
|
||||
if not b then
|
||||
return PaletteFX.pal(self.data, "GREENBAR") or pals.GREENBAR
|
||||
end
|
||||
local hp = b.shownHP or b.mon.hp
|
||||
return pals[PaletteFX.barPalName(hp, b.mon.stats.hp, b.shownPx)]
|
||||
return PaletteFX.pal(self.data,
|
||||
PaletteFX.barPalName(hp, b.mon.stats.hp, b.shownPx))
|
||||
or pals.GREENBAR
|
||||
end
|
||||
local function mon(b, placeholder)
|
||||
if placeholder or not b then return pals.MEWMON or pals.GREENBAR end
|
||||
if placeholder or not b then
|
||||
if PaletteFX.usesYellowCgb() then return pals.MEWMON or pals.GREENBAR end
|
||||
return PaletteFX.pal(self.data, "MEWMON") or pals.MEWMON or pals.GREENBAR
|
||||
end
|
||||
return PaletteFX.monPal(self.data, b.mon.species) or pals.MEWMON
|
||||
end
|
||||
local out = {
|
||||
@@ -5365,23 +5370,6 @@ function BattleState:sgbBattlePals()
|
||||
[2] = mon(self.player, self.showPlayerBack or self.safari or self.demo),
|
||||
[3] = mon(self.enemy, self.showEnemyTrainer),
|
||||
}
|
||||
-- OG RED: the Game Boy Color drew the whole battle from one BG palette --
|
||||
-- white paper, black ink -- so every zone shares the same background and
|
||||
-- outline; only the two mid shades differ per element (green HP bar, red
|
||||
-- mon pic). The bar/base zones otherwise carry the SGB off-white
|
||||
-- (255,239,255) as color 0 while the mon zones (monPal -> GBC_BG) carry a
|
||||
-- true white, which is what drew a white box around each pic on the pink
|
||||
-- field. Snap every zone's color 0/3 to the global GBC white/black; the
|
||||
-- mid shades (and the green bar the user prefers) stay untouched.
|
||||
-- Boot-ROM OG only (Red/Blue): snap zone paper to the global GBC white/black.
|
||||
-- OG YELLOW keeps each CGBBasePalettes endpoint (already near-white / near-black).
|
||||
if PaletteFX.mode == "ogred" and not require("src.core.GameVersion").isYellow() then
|
||||
local white, black = PaletteFX.GBC_BG[1], PaletteFX.GBC_BG[4]
|
||||
for i = 0, 3 do
|
||||
local c = out[i]
|
||||
out[i] = { white, c[2], c[3], black }
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
@@ -5475,23 +5463,29 @@ function BattleState:drawZonePass(src, sx, sy)
|
||||
love.graphics.setShader()
|
||||
end
|
||||
|
||||
-- colors for one anim-layer OAM sprite at screen pixel (px, py): the
|
||||
-- zone palette under that pixel's 8x8 attribute cell (the SGB colors
|
||||
-- the composited picture per cell, so AnimPlayer samples once per cell
|
||||
-- the tile overlaps), through the OBJ palette the routine ran with
|
||||
-- (SetAnimationPalette: wAnimPalette = $f0 on SGB, rOBP1 = $6c,
|
||||
-- ambient rOBP0 = $e4)
|
||||
-- SetAnimationPalette (engine/battle/animations.asm:551): wAnimPalette = $f0
|
||||
-- on SGB, $e4 otherwise; rOBP1 = $6c either way
|
||||
local OBJ_SHADES = {
|
||||
f0 = { 0, 3, 3 }, -- color 1 -> shade 0, colors 2/3 -> shade 3
|
||||
f0x = { 3, 0, 3 }, -- $f0 xor %00111100 = $cc: the Master/Ultra ball
|
||||
-- toss flicker (DoBallTossSpecialEffects)
|
||||
f0x = { 3, 0, 3 }, -- $f0 xor %00111100 = $cc (DoBallTossSpecialEffects,
|
||||
-- engine/battle/animations.asm:685)
|
||||
e4 = { 1, 2, 3 }, -- identity
|
||||
e4x = { 2, 1, 3 }, -- $e4 xor %00111100 = $d8
|
||||
obp1 = { 3, 2, 1 }, -- $6c
|
||||
}
|
||||
function BattleState:animSpriteColors(s, px, py)
|
||||
local P = self:zoneColorsAt(px or (s.x - 8 + 4), py or (s.y - 16 + 4))
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local key = s.obp or "f0"
|
||||
local P
|
||||
-- engine/battle/animations.asm:551 (.notSGB)
|
||||
if PaletteFX.usesSpriteObp() then
|
||||
P = PaletteFX.ogObj()
|
||||
if key == "f0" then key = "e4" elseif key == "f0x" then key = "e4x" end
|
||||
else
|
||||
P = self:zoneColorsAt(px or (s.x - 8 + 4), py or (s.y - 16 + 4))
|
||||
end
|
||||
if not P then return nil end
|
||||
local m = OBJ_SHADES[s.obp or "f0"] or OBJ_SHADES.f0
|
||||
local m = OBJ_SHADES[key] or OBJ_SHADES.f0
|
||||
local function c(shade)
|
||||
local col = P[shade + 1]
|
||||
return { col[1] / 255, col[2] / 255, col[3] / 255 }
|
||||
|
||||
@@ -279,7 +279,7 @@ end
|
||||
|
||||
C.dropsub = function(self)
|
||||
local side = self.env.battleTurn == 0 and "player" or "enemy"
|
||||
self.picOverride[side] = nil
|
||||
self.picOverride[side] = false
|
||||
end
|
||||
|
||||
-- BattleAnimCmd_MinimizeOpp / GetMinimizePic: despite the name it shrinks the
|
||||
|
||||
@@ -1517,10 +1517,10 @@ function Battle:useMove(attacker, defender, moveId)
|
||||
-- read and cleared by the very next move aimed at it, and while it is up the
|
||||
-- accuracy roll does not happen at all.
|
||||
local locked = self:consumeLockOn(defender)
|
||||
-- SUBSTATUS_X_ACCURACY (an X ACCURACY) grants the same roll bypass,
|
||||
-- CheckHit's `.XAccuracy` arm, but is not consumed: it lasts until the
|
||||
-- switch drops the volatile.
|
||||
-- CheckHit's .XAccuracy and EFFECT_ALWAYS_HIT arms
|
||||
-- (effect_commands.asm:1572-1579).
|
||||
local sureHit = locked or self:volatile(attacker).xAccuracy == true
|
||||
or def.effect == "EFFECT_ALWAYS_HIT"
|
||||
|
||||
-- .LockOn runs ahead of .FlyDigMoves and returns a HIT unless the target is
|
||||
-- flying and the move is one of the three (effect_commands.asm:1563-1567,
|
||||
@@ -1723,9 +1723,14 @@ function Battle:useMove(attacker, defender, moveId)
|
||||
text = ("Hit %d time(s)!"):format(landed) })
|
||||
end
|
||||
|
||||
-- Recoil is a quarter of what was dealt; drain heals half of it. Dream
|
||||
-- Eater's sleep requirement is checkhit's, not this block's, so by the
|
||||
-- time a drain is paid out the target is known to have been asleep.
|
||||
-- move_effects/pay_day.asm:13
|
||||
if def.effect == "EFFECT_PAY_DAY" and dealt > 0 then
|
||||
self.payDay = (self.payDay or 0) + 2 * (attacker.level or 1)
|
||||
self:emit({ kind = "message",
|
||||
text = Strings("Coins scattered\neverywhere!") })
|
||||
end
|
||||
|
||||
-- Recoil is a quarter of what was dealt; drain heals half of it.
|
||||
if def.effect == "EFFECT_RECOIL_HIT" and dealt > 0 then
|
||||
local recoil = Effects.recoilDamage(dealt)
|
||||
attacker.hp = math.max(0, (attacker.hp or 0) - recoil)
|
||||
@@ -3012,6 +3017,14 @@ function Battle:resolveFaints()
|
||||
text = (self.trainer.name or "TRAINER") .. " was defeated!" })
|
||||
self:awardPrizeMoney()
|
||||
end
|
||||
-- CheckPayDay, on the win arm only (engine/battle/core.asm:7971-7976,
|
||||
-- :8014-8042).
|
||||
local coins = Prize.payDay(self.save, self.payDay, self.amuletCoin)
|
||||
if coins then
|
||||
self:emit({ kind = "money", text = Prize.payDayMessage(coins,
|
||||
self.save.player and self.save.player.name) })
|
||||
end
|
||||
self.payDay = nil
|
||||
self:endBattle("win")
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -66,6 +66,8 @@ local SENT_SOME = Strings.source("%s got %s%d for winning! Sent some to MOM!")
|
||||
-- see because BankOfMom only ever writes MOM_SAVING_SOME_MONEY_F.
|
||||
local SENT_HALF = Strings.source("Sent half to MOM!")
|
||||
local SENT_ALL = Strings.source("Sent all to MOM!")
|
||||
-- BattleText_PlayerPickedUpPayDayMoney (data/text/battle.asm:3-8).
|
||||
local PICKED_UP = Strings.source("%s picked up %s%d!")
|
||||
|
||||
-- charmap.asm: the currency glyph, the same one Chrome.money floats in front
|
||||
-- of a six-digit field.
|
||||
@@ -220,4 +222,17 @@ function Prize.message(award, playerName)
|
||||
return Strings(GOT_MONEY, name, YEN, total)
|
||||
end
|
||||
|
||||
-- CheckPayDay (engine/battle/core.asm:8014-8042): the Amulet Coin doubles the
|
||||
-- accumulated total once, and the wallet is written directly, no Mom split.
|
||||
function Prize.payDay(save, amount, amuletCoin)
|
||||
if not (save and save.player) or (amount or 0) <= 0 then return nil end
|
||||
if amuletCoin then amount = amount * 2 end
|
||||
setPlayerMoney(save, addToAccount(playerMoney(save), amount))
|
||||
return amount
|
||||
end
|
||||
|
||||
function Prize.payDayMessage(amount, playerName)
|
||||
return Strings(PICKED_UP, playerName or "PLAYER", YEN, amount or 0)
|
||||
end
|
||||
|
||||
return Prize
|
||||
|
||||
+9
-2
@@ -730,7 +730,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 +740,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 +888,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
|
||||
@@ -1949,6 +1955,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.
|
||||
|
||||
+38
-7
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -560,6 +560,16 @@ local function gen2Rows(opts, hooks)
|
||||
end)
|
||||
end
|
||||
|
||||
local okVm, VideoMode = pcall(require, "src.core.VideoMode")
|
||||
if okVm then
|
||||
add(Strings("VIDEO MODE"),
|
||||
function() return VideoMode.modeLabel(opts.videoMode) end,
|
||||
function(dir)
|
||||
opts.videoMode = VideoMode.cycle(opts.videoMode, dir)
|
||||
return true
|
||||
end)
|
||||
end
|
||||
|
||||
addTouchRows(rows, add, opts, hooks)
|
||||
|
||||
return rows
|
||||
|
||||
+17
-20
@@ -119,13 +119,9 @@ PaletteFX.GBC_OBJ_BLUE = {
|
||||
{ 255, 255, 255 }, { 255, 132, 132 }, { 148, 58, 58 }, { 0, 0, 0 },
|
||||
}
|
||||
|
||||
-- The active game's OG boot-ROM background palette: blue for a Blue
|
||||
-- playthrough, red for Red. White (index 1) and black (index 4) are
|
||||
-- identical across Red/Blue, so callers that only touch the endpoints
|
||||
-- (e.g. BattleState's zone white/black snap) need no version branch there.
|
||||
-- Yellow is CGB-enhanced (pokeyellow CGBBasePalettes): named zones go through
|
||||
-- pal() / usesYellowCgb(), and ogBg() falls back to CGBBase PAL_ROUTE for any
|
||||
-- remaining whole-screen callers -- never Blue's GBC_BG_BLUE.
|
||||
-- The active game's OG boot-ROM background palette: blue for Blue, red for Red.
|
||||
-- Yellow is CGB-enhanced (pokeyellow CGBBasePalettes), so ogBg() falls back to
|
||||
-- CGBBase PAL_ROUTE there, never Blue's GBC_BG_BLUE.
|
||||
function PaletteFX.ogBg()
|
||||
if GameVersion.isBlue() then return PaletteFX.GBC_BG_BLUE end
|
||||
if GameVersion.isYellow() then
|
||||
@@ -354,23 +350,14 @@ function PaletteFX.usesSpriteObp(mode)
|
||||
return mode == "ogred" and not GameVersion.isYellow()
|
||||
end
|
||||
|
||||
-- ------- post-zone sprite redraw (OG RED)
|
||||
--
|
||||
-- In OG RED the world canvas still runs through the whole-screen zone
|
||||
-- shade-remap shader, which would corrupt an OBP-baked sprite's true-color
|
||||
-- pixels. (SGB used to come through here too; it no longer bakes an object
|
||||
-- palette at all, so its characters are colorized by the zone like the ground
|
||||
-- they stand on and never queue a replay -- see usesSpriteObp, #301.) So SpriteRenderer draws the baked sprite into the canvas (its
|
||||
-- pixels come out zone-tinted there) AND records the draw here;
|
||||
-- Renderer:endFrame replays the list on top of the finished zone pass,
|
||||
-- scaled into screen space -- the GBC's OBJ-over-BG compositing, one draw
|
||||
-- late. Entries carrying `colors` are re-colorized draws (the tall-grass
|
||||
-- feet overdraw, which must keep hiding sprite feet) issued through the
|
||||
-- color-0-keyed shade-remap shader. World pass only; cleared per frame.
|
||||
-- ------- post-zone sprite redraw (OG RED): OBP-baked draws recorded here and
|
||||
-- replayed by Renderer:endFrame on top of the zone pass (usesSpriteObp, #301)
|
||||
local spriteRedraws = {}
|
||||
local uiSpriteRedraws = {}
|
||||
|
||||
function PaletteFX.clearSpriteRedraws()
|
||||
for i = #spriteRedraws, 1, -1 do spriteRedraws[i] = nil end
|
||||
for i = #uiSpriteRedraws, 1, -1 do uiSpriteRedraws[i] = nil end
|
||||
end
|
||||
|
||||
function PaletteFX.markSpriteRedraw(image, quad, x, y, sx, colors, keyed)
|
||||
@@ -392,6 +379,16 @@ function PaletteFX.spriteRedraws()
|
||||
return spriteRedraws
|
||||
end
|
||||
|
||||
function PaletteFX.markUiSpriteRedraw(image, quad, x, y)
|
||||
if currentPass ~= "ui" then return end
|
||||
uiSpriteRedraws[#uiSpriteRedraws + 1] =
|
||||
{ image = image, quad = quad, x = x + markOffsetX, y = y }
|
||||
end
|
||||
|
||||
function PaletteFX.uiSpriteRedraws()
|
||||
return uiSpriteRedraws
|
||||
end
|
||||
|
||||
-- Active named-palette table for COLORS: RED++ uses data/palettes_gbc.lua,
|
||||
-- everything else uses the ROM-imported data.palettes.
|
||||
function PaletteFX.pack(data)
|
||||
|
||||
@@ -1017,6 +1017,20 @@ function Renderer:endFrame(zones, worldZones)
|
||||
p.dx - p.a.x * Ux, p.dy - p.a.y * Uy, p.dx, p.dy, p.dw, p.dh)
|
||||
end
|
||||
end
|
||||
local uiRedraws = PaletteFX.uiSpriteRedraws()
|
||||
if uiRedraws[1] then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.setScissor(uox, uoy, uvpw, uvph)
|
||||
for _, r in ipairs(uiRedraws) do
|
||||
if r.quad then
|
||||
love.graphics.draw(r.image, r.quad, uox + r.x * Ux, uoy + r.y * Uy,
|
||||
0, Ux, Uy)
|
||||
else
|
||||
love.graphics.draw(r.image, uox + r.x * Ux, uoy + r.y * Uy, 0, Ux, Uy)
|
||||
end
|
||||
end
|
||||
love.graphics.setScissor()
|
||||
end
|
||||
|
||||
-- The battle wipe covers the whole surface, letterbox included, so it goes
|
||||
-- over the finished composite rather than under the UI blit. On hardware
|
||||
|
||||
@@ -51,6 +51,7 @@ function TextBox.new(game, text, onDone, opts)
|
||||
self.choice = opts and opts.choice
|
||||
self.defaultNo = opts and opts.defaultNo
|
||||
self.choiceNoSound = opts and opts.noSound
|
||||
self.money = opts and opts.money
|
||||
self.auto = opts and opts.auto
|
||||
self.stay = opts and opts.stay
|
||||
-- opts.instant: put the LAST page up already typed, with no typewriter and
|
||||
@@ -467,6 +468,14 @@ function TextBox:draw()
|
||||
pen = pen + Font.advanceOf(code)
|
||||
end
|
||||
end
|
||||
if self.money then
|
||||
-- money box (engine/menus/text_box.asm:130): DisplayMoneyBox at
|
||||
-- hlcoord 11,0, the amount right-aligned on its middle row
|
||||
Font.drawBox(11, 0, 9, 3)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
local money = ("¥%d"):format(self.money() or 0)
|
||||
Font.draw(money, 152 - Font.width(money), 8)
|
||||
end
|
||||
if (self.waiting or (self.done and not self.choice and not self.auto
|
||||
and not self.stay))
|
||||
and self.blink < 30 then
|
||||
|
||||
@@ -565,10 +565,11 @@ end
|
||||
|
||||
-- ---- engine/events/poisonstep.asm -----------------------------------------
|
||||
|
||||
-- .PlayPoisonSFX: SFX_POISON, then LoadPoisonBGPals for two frames. The port
|
||||
-- plays the sound; the two-frame red flash is the renderer's business.
|
||||
-- .PlayPoisonSFX: SFX_POISON, then LoadPoisonBGPals for four frames.
|
||||
-- engine/events/poisonstep.asm:101
|
||||
function H.PlayPoisonSFX(ctx)
|
||||
call(ctx, "playSfxNamed", SFX_POISON[1], SFX_POISON[2])
|
||||
call(ctx, "poisonBGFlash")
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
@@ -134,13 +134,15 @@ function SummaryMenu:draw()
|
||||
Font.draw(("%03d"):format(def.dex or 0), 24, 56)
|
||||
|
||||
if self.page == 1 then
|
||||
-- HP bar (11,3) + numbers row 4, STATUS/ (9,6), the DrawLineBox
|
||||
-- bracket around the name/HP block, and PrintLevel at (14,2). The
|
||||
-- level belongs to page 1 ONLY: StatusScreen2 opens with ClearScreenArea
|
||||
-- over (9,2) 5x10 (status_screen.asm:303-305), which wipes it. #280
|
||||
-- level is page 1 only: StatusScreen2 opens with ClearScreenArea over
|
||||
-- (9,2) 5x10 (status_screen.asm:303-305). #280
|
||||
printLevel(14, 2, mon.level)
|
||||
drawLineBox(19, 1, 6, 10)
|
||||
HudTiles.drawHPBar(data, 11, 3, mon, 1) -- wHPBarType 1
|
||||
-- engine/pokemon/status_screen.asm:120-125
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
local barZoned = PaletteFX.shader() ~= nil
|
||||
and PaletteFX.pal(data, "GREENBAR") ~= nil
|
||||
HudTiles.drawHPBar(data, 11, 3, mon, 1, barZoned) -- wHPBarType 1
|
||||
Font.draw(("%3d/%3d"):format(mon.hp, mon.stats.hp), 96, 32)
|
||||
Font.draw(Strings("STATUS/"), 72, 48)
|
||||
Font.draw(mon.status or "OK", 128, 48)
|
||||
|
||||
+27
-4
@@ -171,6 +171,27 @@ local function markVisibleTrueColor(x, y, w, h, cover)
|
||||
if ix2 < right then P.markTrueColor(ix2, iy1, right - ix2, iy2 - iy1) end
|
||||
end
|
||||
|
||||
-- ..(engine/movie/title.asm ln 321)
|
||||
local function replayObjSprite(game, image, quad, x, y)
|
||||
local P = require("src.render.PaletteFX")
|
||||
if not P.usesSpriteObp() then return end
|
||||
local top = game.stack and game.stack:top()
|
||||
local box = top and top.titleUiBox
|
||||
if box then
|
||||
local w, h
|
||||
if quad then
|
||||
w, h = select(3, quad:getViewport())
|
||||
else
|
||||
w, h = image:getDimensions()
|
||||
end
|
||||
if x < (box[3] + 1) * 8 and x + w > box[1] * 8
|
||||
and y < (box[4] + 1) * 8 and y + h > box[2] * 8 then
|
||||
return
|
||||
end
|
||||
end
|
||||
P.markUiSpriteRedraw(image, quad, x, y)
|
||||
end
|
||||
|
||||
function TitleState.new(game, opts)
|
||||
opts = opts or {}
|
||||
local self = setmetatable({}, TitleState)
|
||||
@@ -662,12 +683,11 @@ function TitleState:draw()
|
||||
local x = 40 + math.floor((56 - w) / 2) + self.monOffset
|
||||
local y = 136 - h
|
||||
love.graphics.draw(sprite, x, y)
|
||||
-- a full-color mon keeps its own palette through the SGB pass, minus
|
||||
-- the strip Red's OAM covers (#350). Yellow never reaches here: its
|
||||
-- layout has no cycling mon and no Red art (title_yellow.asm).
|
||||
-- SGB: the mon keeps its palette minus the strip Red's OAM covers
|
||||
-- (#350); Yellow never reaches here (title_yellow.asm)
|
||||
if spriteTrueColor then
|
||||
local cover
|
||||
if playerImage then
|
||||
if playerImage and not require("src.render.PaletteFX").usesSpriteObp() then
|
||||
local pw, ph = playerImage:getDimensions()
|
||||
cover = { 82, 80, pw, ph }
|
||||
end
|
||||
@@ -678,10 +698,13 @@ function TitleState:draw()
|
||||
if self.playerQuads then
|
||||
for _, part in ipairs(self.playerQuads) do
|
||||
love.graphics.draw(playerImage, part[1], 82 + part[2], 80 + part[3])
|
||||
replayObjSprite(self.game, playerImage, part[1], 82 + part[2], 80 + part[3])
|
||||
end
|
||||
love.graphics.draw(playerImage, self.ballQuad, 82, self.ballY)
|
||||
replayObjSprite(self.game, playerImage, self.ballQuad, 82, self.ballY)
|
||||
elseif playerImage then
|
||||
love.graphics.draw(playerImage, 82, 80)
|
||||
replayObjSprite(self.game, playerImage, nil, 82, 80)
|
||||
end
|
||||
end
|
||||
self:drawCopyright(136 + (preRibbon and 0 or scrollY))
|
||||
|
||||
+63
-25
@@ -31,6 +31,7 @@ local ItemEffects = require("src.core.gen2.ItemEffects")
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local Palettes = require("src.world.gen2.Palettes")
|
||||
local Pokerus = require("src.core.gen2.Pokerus")
|
||||
local Prize = require("src.battle.gen2.Prize")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local Screens = require("src.ui.Screens")
|
||||
local Sound = require("src.core.Sound")
|
||||
@@ -623,6 +624,19 @@ function BattleState:drawPic(mon, back)
|
||||
and not (self.vanishAnim and self.vanishAnim == self.anim) then
|
||||
return
|
||||
end
|
||||
-- GetSubstitutePic (engine/battle_anims/anim_commands.asm:905-960): the
|
||||
-- doll sits in the mon's own pic box and takes its palette.
|
||||
local doll, dollQuad
|
||||
if not (trainerBack or enemyTrainer) then
|
||||
local over = anim and anim.pic
|
||||
local up
|
||||
if over ~= nil then
|
||||
up = over == "substitute"
|
||||
else
|
||||
up = mon and mon.volatile and (mon.volatile.substitute or 0) > 0
|
||||
end
|
||||
if up then doll, dollQuad = self:substituteDoll(back) end
|
||||
end
|
||||
local G = love.graphics
|
||||
local w, h = image:getDimensions()
|
||||
local px, py
|
||||
@@ -662,6 +676,10 @@ function BattleState:drawPic(mon, back)
|
||||
px = px + math.floor(w * (1 - scale) / 2)
|
||||
py = py + math.floor(h * (1 - scale))
|
||||
end
|
||||
if doll then
|
||||
px = (back and 32 or 112) + ((anim and anim.slide) or 0)
|
||||
py = back and 80 or 40
|
||||
end
|
||||
G.setColor(1, 1, 1, 1)
|
||||
-- No mon on this side at all in the catching tutorial, where the box holds
|
||||
-- the DUDE's back-pic and nothing else for the whole battle.
|
||||
@@ -684,6 +702,10 @@ function BattleState:drawPic(mon, back)
|
||||
-- what is still inside the box rather than drawn over the HUD below it.
|
||||
local sunk = self:faintSink(side)
|
||||
local function body()
|
||||
if doll then
|
||||
G.draw(doll, dollQuad, px, py)
|
||||
return
|
||||
end
|
||||
if sunk > 0 then
|
||||
local visible = h - math.floor(sunk / scale)
|
||||
if visible <= 0 then return end
|
||||
@@ -703,6 +725,24 @@ function BattleState:drawPic(mon, back)
|
||||
end
|
||||
end
|
||||
|
||||
-- MonsterSpriteGFX (gfx/sprites.asm:82): the facing-DOWN 16x16 frame for the
|
||||
-- enemy's frontpic, facing-UP for the player's backpic.
|
||||
function BattleState:substituteDoll(back)
|
||||
if self.subDoll == nil then
|
||||
local ok, image = pcall(Assets.image, "assets/generated/sprites/monster.png")
|
||||
if ok and image then
|
||||
local w, h = image:getDimensions()
|
||||
self.subDoll = { image = image,
|
||||
down = love.graphics.newQuad(0, 0, 16, 16, w, h),
|
||||
up = love.graphics.newQuad(0, 16, 16, 16, w, h) }
|
||||
else
|
||||
self.subDoll = false
|
||||
end
|
||||
end
|
||||
if not self.subDoll then return nil end
|
||||
return self.subDoll.image, back and self.subDoll.up or self.subDoll.down
|
||||
end
|
||||
|
||||
-- The top `visible` rows of a pic, for the faint slide. One quad, re-aimed,
|
||||
-- the way BattleAnimView keeps one blit quad rather than a new one per frame.
|
||||
function BattleState:cropQuad(image, visible)
|
||||
@@ -1074,16 +1114,8 @@ function BattleState:stepAnim(input)
|
||||
end
|
||||
end
|
||||
|
||||
-- NOTE on picHidden and the animation runtime. An animation that ENDS with a
|
||||
-- pic box cleared could latch it here, and the tilemap argument says it should:
|
||||
-- BattleAnimRestoreHuds redraws the two HUDs and nothing else. It deliberately
|
||||
-- does not, because BATTLE_BG_EFFECT_REMOVE_MON and _RETURN_MON are also used
|
||||
-- by moves whose user is still standing there afterwards -- SUBSTITUTE (the
|
||||
-- doll takes the box over, and nothing in this port draws one yet), SKY_ATTACK,
|
||||
-- BEAT_UP, BATON_PASS -- and a blanket latch would make those mons invisible
|
||||
-- for the rest of the fight. The two moments the cart really does leave the
|
||||
-- box empty for good are latched explicitly instead: a catch (pushCaught) and
|
||||
-- a faint (MonFaintedAnimation, in update).
|
||||
-- REMOVE_MON / RETURN_MON also serve SUBSTITUTE, SKY_ATTACK, BEAT_UP and
|
||||
-- BATON_PASS, so picHidden is only latched by a catch and a faint.
|
||||
|
||||
-- Whatever Call_PlayBattleAnim was standing in front of: a send-out's cry and
|
||||
-- HUD update run the moment its animation is done, cut short or not.
|
||||
@@ -1103,6 +1135,7 @@ function BattleState:animPicState(side)
|
||||
size = bg.picSize[side],
|
||||
slide = bg.slide[side] or 0,
|
||||
shade = bg.monShade[side],
|
||||
pic = self.anim.picOverride[side],
|
||||
}
|
||||
end
|
||||
|
||||
@@ -1192,22 +1225,14 @@ function BattleState:advanceQueue()
|
||||
if event.kind == "level" and event.index then
|
||||
self.evolvable[event.index] = true
|
||||
-- GiveExperiencePoints' `.skip_active_mon_update` guard
|
||||
-- (engine/battle/core.asm:6999-7003): only the mon that is OUT copies its
|
||||
-- recalculated HP, max HP and level into the battle struct, and only then
|
||||
-- does `callfar UpdatePlayerHUD` (:7034) redraw the bar. That is a
|
||||
-- REDRAW, not AnimateHPBar, so the shown HP snaps instead of chasing --
|
||||
-- without it the bar kept the pre-level-up HP against the new maximum
|
||||
-- until the next damage or heal event moved it.
|
||||
-- (engine/battle/core.asm:6999-7003): the OUT mon's shown HP snaps.
|
||||
local battle = self.battle
|
||||
local mon = battle and battle.party and battle.party[event.index]
|
||||
-- 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
|
||||
-- engine/battle/core.asm:7044
|
||||
-- engine/battle/core.asm:7284
|
||||
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
|
||||
@@ -1215,9 +1240,6 @@ function BattleState:advanceQueue()
|
||||
end
|
||||
end
|
||||
-- `ld [wBattleMonLevel], a` in the same guarded block (:7018-7020).
|
||||
-- AnimateExpBar has already walked the number up one level at a time by
|
||||
-- the time this runs, so this only catches a level gained with no exp
|
||||
-- crawl behind it.
|
||||
self.shownLevel = mon.level or self.shownLevel
|
||||
end
|
||||
end
|
||||
@@ -2484,7 +2506,10 @@ function BattleState:pushCaught(enemy, itemId)
|
||||
self:push({ kind = "dex-entry", species = enemy.species })
|
||||
end
|
||||
if self.contest then
|
||||
return self:contestCatch(enemy)
|
||||
-- A contest catch still exits through the `.run` arm with result WIN
|
||||
-- (engine/battle/core.asm:4780-4783), so CheckPayDay runs for it too.
|
||||
self:contestCatch(enemy)
|
||||
return self:pushPayDay()
|
||||
end
|
||||
save.party = save.party or {}
|
||||
local toPc = #save.party >= Boxes.PARTY_SIZE
|
||||
@@ -2540,6 +2565,19 @@ function BattleState:pushCaught(enemy, itemId)
|
||||
self:push({ kind = "message",
|
||||
text = self:name(enemy) .. " was sent to BILL's PC." })
|
||||
end
|
||||
self:pushPayDay()
|
||||
end
|
||||
|
||||
-- CheckPayDay runs on a capture too: `and $f` keeps the win arm
|
||||
-- (engine/battle/core.asm:7971-7976, :8014-8042).
|
||||
function BattleState:pushPayDay()
|
||||
local save = self.save
|
||||
local coins = Prize.payDay(save, self.battle.payDay, self.battle.amuletCoin)
|
||||
self.battle.payDay = nil
|
||||
if coins then
|
||||
self:push({ kind = "message",
|
||||
text = Prize.payDayMessage(coins, save.player and save.player.name) })
|
||||
end
|
||||
end
|
||||
|
||||
-- CheckWhetherToAskSwitch: a started battle, more than one mon, no link, the
|
||||
@@ -3049,7 +3087,7 @@ function BattleState:applyPartyItem(itemId, action, mon, slot)
|
||||
local before = (mon and mon.hp) or 0
|
||||
local result
|
||||
if action == "pp" then
|
||||
result = ItemEffects.usePpItem(itemId, mon, slot)
|
||||
result = ItemEffects.usePpItem(itemId, mon, slot, data)
|
||||
else
|
||||
result = ItemEffects.useOnMon(itemId, mon, data)
|
||||
end
|
||||
|
||||
@@ -172,6 +172,17 @@ local ROWS = {
|
||||
text = function(options)
|
||||
return require("src.render.GBCFX").levelLabel(options.gbcfx or 0)
|
||||
end },
|
||||
{ label = "VIDEO MODE", key = "videoMode", port = true,
|
||||
cycle = function(options, delta)
|
||||
local VideoMode = require("src.core.VideoMode")
|
||||
options.videoMode = VideoMode.cycle(options.videoMode, delta)
|
||||
VideoMode.apply(options.videoMode)
|
||||
end,
|
||||
text = function(options)
|
||||
local VideoMode = require("src.core.VideoMode")
|
||||
return VideoMode.normalize(options.videoMode) == "borderless"
|
||||
and "FULL" or "WINDOWED"
|
||||
end },
|
||||
{ id = "touchControls", label = "TOUCH PAD", port = true,
|
||||
text = function(options)
|
||||
local tc = options.touchControls
|
||||
|
||||
@@ -283,6 +283,14 @@ function PartyMenu:finishSwitch()
|
||||
if self.save and self.save.party == party then
|
||||
Mail.swapSlots(self.save, from, to)
|
||||
end
|
||||
-- engine/pokemon/switchpartymons.asm:38
|
||||
local data = self.game and self.game.data
|
||||
local ok, Sound = pcall(require, "src.core.Sound")
|
||||
if not (ok and data and Sound and Sound.play) then return end
|
||||
local sfx = data.audio and data.audio.sfx
|
||||
if sfx and sfx[Sound.resolve(data, "Sfx_SwitchPokemon")] then
|
||||
pcall(Sound.play, data, "Sfx_SwitchPokemon")
|
||||
end
|
||||
end
|
||||
|
||||
-- The reopened list: InitPartyMenuNoCancel caps the cursor at the last mon,
|
||||
|
||||
@@ -1969,8 +1969,11 @@ function OverworldState:tryBookshelf(fx, fy)
|
||||
return true
|
||||
end
|
||||
if entry.screen then
|
||||
-- Blue's house shelf opens the TOWN MAP (TownMapText)
|
||||
pcall(Screens.push, Game, entry.screen)
|
||||
-- engine/events/hidden_events/town_map.asm:1
|
||||
Game.stack:push(TextBox.new(Game,
|
||||
t[entry.text] or t._TownMapText
|
||||
or romText(Game.data, "_TownMapText", "A TOWN MAP."),
|
||||
function() pcall(Screens.push, Game, entry.screen) end))
|
||||
return true
|
||||
end
|
||||
local kind = entry.kind
|
||||
@@ -2136,14 +2139,17 @@ function OverworldState:tryHiddenObject(fx, fy)
|
||||
for _, h in ipairs(extras.pcTiles[self.map.id] or {}) do
|
||||
if h.x == fx and h.y == fy and (not h.facing or h.facing == facing) then
|
||||
if self.map.id == "REDS_HOUSE_2F" then
|
||||
-- The player's bedroom PC is the one location in Red/Blue whose PC
|
||||
-- callback is OpenRedsPC (engine/events/hidden_objects/players_pc.asm),
|
||||
-- which runs the PlayerPC predef directly -- item storage, no
|
||||
-- SOMEONE'S/BILL'S PC main menu (DisplayPCMainMenu). Every other
|
||||
-- pcTile is a Pokémon Center-style PC that shows the multi-PC menu. (#228)
|
||||
-- OpenRedsPC (engine/events/hidden_objects/players_pc.asm) runs the
|
||||
-- PlayerPC predef directly, no DisplayPCMainMenu (#228)
|
||||
require("src.core.Sound").play(Game.data, "Turn_On_PC")
|
||||
-- direct access: ExitPlayerPC rings SFX_TURN_OFF_PC (players_pc.asm, #960)
|
||||
Screens.push(Game, "PlayerPC", { direct = true })
|
||||
-- engine/menus/players_pc.asm:16
|
||||
Game.stack:push(TextBox.new(Game,
|
||||
(Game.data.text or {})._TurnedOnPC2Text
|
||||
or romText(Game.data, "_TurnedOnPC2Text", "{PLAYER} turned on\nthe PC."),
|
||||
function()
|
||||
-- direct access: ExitPlayerPC rings SFX_TURN_OFF_PC (players_pc.asm, #960)
|
||||
Screens.push(Game, "PlayerPC", { direct = true })
|
||||
end, { instant = true }))
|
||||
else
|
||||
self:openPC()
|
||||
end
|
||||
@@ -2892,13 +2898,16 @@ function OverworldState:openPC(onDone)
|
||||
done()
|
||||
end
|
||||
table.insert(items, { label = Strings("LOG OFF"), onSelect = logOff })
|
||||
-- pokered sets BIT_NO_MENU_BUTTON_SOUND for the whole PC session
|
||||
-- (engine/overworld/pokecenter_pc.asm / player_pc.asm); DisplayPCMainMenu
|
||||
-- calls TextBoxBorder with c=14 (interior width, +2 for the border), so
|
||||
-- tw here (total width) is 16
|
||||
Game.stack:push(Menu.new(Game, items,
|
||||
-- BIT_NO_MENU_BUTTON_SOUND for the whole PC session; DisplayPCMainMenu's
|
||||
-- TextBoxBorder c=14 interior -> tw 16 (engine/overworld/pokecenter_pc.asm)
|
||||
local menu = Menu.new(Game, items,
|
||||
{ tx = 0, ty = 0, tw = 16, th = #items * 2 + 2, onCancel = logOff,
|
||||
noSound = true }))
|
||||
noSound = true })
|
||||
-- engine/menus/pc.asm:5
|
||||
Game.stack:push(TextBox.new(Game,
|
||||
(Game.data.text or {})._TurnedOnPC1Text
|
||||
or romText(Game.data, "_TurnedOnPC1Text", "{PLAYER} turned on\nthe PC."),
|
||||
function() Game.stack:push(menu) end))
|
||||
end
|
||||
|
||||
-- The PROF. OAK's PC session (engine/menus/oaks_pc.asm OpenOaksPC): the
|
||||
|
||||
@@ -78,7 +78,7 @@ function StepEvents.poisonStep(party)
|
||||
return { kind = "poisonFaint", fainted = fainted, hurt = hurt, blocks = true }
|
||||
end
|
||||
if #hurt > 0 then
|
||||
-- .PlayPoisonSFX and the two-frame red flash, then `xor a`: no carry, so
|
||||
-- .PlayPoisonSFX and the four-frame BG flash, then `xor a`: no carry, so
|
||||
-- the step still counts and the wild roll still happens.
|
||||
return { kind = "poisonHurt", hurt = hurt, blocks = false }
|
||||
end
|
||||
|
||||
+35
-14
@@ -2123,6 +2123,11 @@ function World:updateShake()
|
||||
shake.phase = (shake.left % 2 == 0) and shake.amplitude or -shake.amplitude
|
||||
end
|
||||
|
||||
-- engine/events/poisonstep_pals.asm:9
|
||||
function World:poisonBGFlash()
|
||||
self.poisonFlash = 4
|
||||
end
|
||||
|
||||
-- Script_warp / Script_warpfacing: a raw destination CELL, distinct from the
|
||||
-- warp_events World:takeWarp follows. `facing` is nil for `warp` and a
|
||||
-- Movement direction for `warpfacing` (PLAYERSPRITESETUP_CUSTOM_FACING). A
|
||||
@@ -2400,7 +2405,9 @@ end
|
||||
function World:playMapMusic()
|
||||
local data = self.game and self.game.data
|
||||
if data and data.audio and data.audio.runtime and self.map then
|
||||
Music.playMap(data, self.map.id)
|
||||
-- SpecialMapMusic (home/audio.asm:397)
|
||||
Music.playMap(data, self.map.id, nil,
|
||||
FieldMoves.isSurfing(self.playerState))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3246,7 +3253,9 @@ function World:surfStartStep(mon)
|
||||
self:applyPlayerState(FieldMoves.surfType(mon))
|
||||
local audio = self.game and self.game.data and self.game.data.audio
|
||||
if audio and audio.runtime and self.map then
|
||||
Music.playMap(self.game.data, self.map.id)
|
||||
-- SpecialMapMusic (home/audio.asm:397)
|
||||
Music.playMap(self.game.data, self.map.id, nil,
|
||||
FieldMoves.isSurfing(self.playerState))
|
||||
end
|
||||
if p.scriptStep then p:scriptStep(p.facing) end
|
||||
self.fieldMove = { phase = "step" }
|
||||
@@ -5363,7 +5372,9 @@ function World:runSurf(result)
|
||||
self:applyPlayerState(result.state)
|
||||
local audio = self.game and self.game.data and self.game.data.audio
|
||||
if audio and audio.runtime and self.map then
|
||||
Music.playMap(self.game.data, self.map.id)
|
||||
-- SpecialMapMusic (home/audio.asm:397)
|
||||
Music.playMap(self.game.data, self.map.id, nil,
|
||||
FieldMoves.isSurfing(self.playerState))
|
||||
end
|
||||
if self.player and self.player.scriptStep then
|
||||
self.player:scriptStep(self.player.facing)
|
||||
@@ -8524,13 +8535,12 @@ function World:setMap(mapId, cx, cy, facing, opts)
|
||||
-- rebuildPeople may have pooled fresh NPCs; give them their colors too.
|
||||
self:applyPalettes()
|
||||
local audio = self.game and self.game.data and self.game.data.audio
|
||||
-- PlayMapMusicBike (home/audio.asm), which is the mapsetup every load uses:
|
||||
-- a player still on the bike keeps the bike theme across the warp instead of
|
||||
-- hearing the new map's song. Music.play dedupes the same label, so this is
|
||||
-- safe on seamless edge crossings.
|
||||
-- PlayMapMusicBike / SpecialMapMusic (home/audio.asm:335, :397): a biking
|
||||
-- player keeps the bike theme; Music.play dedupes seamless edge crossings.
|
||||
if audio and audio.runtime then
|
||||
if not (FieldMoves.isBiking(self.playerState) and self:playBikeMusic()) then
|
||||
Music.playMap(self.game.data, mapId)
|
||||
Music.playMap(self.game.data, mapId, nil,
|
||||
FieldMoves.isSurfing(self.playerState))
|
||||
end
|
||||
end
|
||||
-- Fires with the map fully built and BEFORE the map's own scene script, so a
|
||||
@@ -8912,14 +8922,13 @@ function World:movePlayer(dir)
|
||||
if result == "moved"
|
||||
and Permissions.surfable(map:cellCollision(p.targetX, p.targetY))
|
||||
== "land" then
|
||||
-- .ExitWater: GetOutOfWater writes PLAYER_NORMAL and runs
|
||||
-- UpdatePlayerSprite BEFORE .DoStep, so the player is already off the
|
||||
-- Lapras for the step that puts them on the beach, and PlayMapMusic then
|
||||
-- swaps the surfing theme back for the map's own.
|
||||
-- .ExitWater: GetOutOfWater writes PLAYER_NORMAL before .DoStep, then
|
||||
-- PlayMapMusic swaps the surf theme back (home/audio.asm:308)
|
||||
self:applyPlayerState(FieldMoves.PLAYER_NORMAL)
|
||||
local audio = self.game and self.game.data and self.game.data.audio
|
||||
if audio and audio.runtime then
|
||||
Music.playMap(self.game.data, map.id)
|
||||
Music.playMap(self.game.data, map.id, nil,
|
||||
FieldMoves.isSurfing(self.playerState))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9159,7 +9168,7 @@ function World:countStep()
|
||||
elseif event.kind == "poisonFaint" then
|
||||
self:poisonFaintScript(event)
|
||||
elseif event.kind == "poisonHurt" then
|
||||
-- .PlayPoisonSFX alone: the sound and the two-frame red flash, no script.
|
||||
-- .PlayPoisonSFX alone: the sound and the four-frame BG flash, no script.
|
||||
CallAsm.run(self, "PlayPoisonSFX")
|
||||
elseif event.kind == "repel" then
|
||||
self:repelWoreOff()
|
||||
@@ -10031,6 +10040,18 @@ function World:draw()
|
||||
G.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
-- engine/events/poisonstep_pals.asm:9-42
|
||||
if self.poisonFlash and self.poisonFlash > 0 then
|
||||
self.poisonFlash = self.poisonFlash - 1
|
||||
if GbcPalette.mode == "gbc" then
|
||||
G.setColor(28 / 31, 21 / 31, 1, 0.55)
|
||||
else
|
||||
G.setColor(0, 0, 0, 0.45)
|
||||
end
|
||||
G.rectangle("fill", 0, 0, w, h)
|
||||
G.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
-- FadeOutToWhite / FadeOutToBlack, held until a FadeInFrom* clears it. On
|
||||
-- the cart the pair brackets a scripted cutscene's set change (the Elite Four
|
||||
-- doors, the Radio Tower takeover, Lugia's chamber); the port has no
|
||||
|
||||
Reference in New Issue
Block a user