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

This commit is contained in:
bryanthaboi
2026-08-16 06:41:56 -04:00
parent 3ee50a27c5
commit 12fdfa1e88
119 changed files with 2555 additions and 42186 deletions
+45 -51
View File
@@ -364,21 +364,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
@@ -2608,6 +2604,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
@@ -2664,7 +2663,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
@@ -2678,6 +2678,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
@@ -3892,9 +3893,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
@@ -5296,24 +5297,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 = {
@@ -5322,23 +5327,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
@@ -5432,23 +5420,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 }
+1 -1
View File
@@ -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
+19 -6
View File
@@ -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
+15
View File
@@ -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