mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-26 15:31:15 +02:00
329 lines
13 KiB
Lua
329 lines
13 KiB
Lua
-- Gen 2 damage.
|
|
--
|
|
-- Ported from engine/battle/effect_commands.asm, in the order the cart's move
|
|
-- sequence runs them: damagestats -> damagecalc -> stab -> damagevariation.
|
|
--
|
|
-- What differs from Gen 1 (src/battle/Damage.lua), and why this is its own
|
|
-- module rather than a flag on that one:
|
|
-- * Special is split into Special Attack and Special Defense, so a special
|
|
-- move reads the attacker's SpA against the defender's SpD instead of both
|
|
-- sides' single `special`.
|
|
-- * Critical hits are a *chance ladder* (data/battle/critical_hit_chances.asm
|
|
-- 1/15, 1/8, 1/4, 1/3, 1/2) indexed by a "critical level" that Focus
|
|
-- Energy, a high-crit move, Scope Lens, and the Lucky Punch / Stick raise.
|
|
-- Gen 1 instead derived the chance from base Speed.
|
|
-- * A critical hit is a flat x2 and, unlike Gen 1, ignores the attacker's
|
|
-- *negative* stat stages rather than all stages.
|
|
-- * Type-boost held items (Charcoal, Mystic Water, ...) multiply before the
|
|
-- crit, and Steel and Dark exist in the matchup table.
|
|
--
|
|
-- Whether a move is physical or special is still decided by its *type*, not
|
|
-- per-move as in Gen 4: type ids below FIRE are physical. type_chart.lua's
|
|
-- records carry that as `category`.
|
|
|
|
local Damage = {}
|
|
|
|
-- data/battle/critical_hit_chances.asm, as "1 in N".
|
|
Damage.CRITICAL_CHANCES = { [0] = 15, 8, 4, 3, 2, 2, 2 }
|
|
|
|
-- Gen 2's damage spread: 85% to 100% inclusive.
|
|
Damage.MIN_VARIATION = 85
|
|
Damage.MAX_VARIATION = 100
|
|
|
|
-- The cart caps a single hit at 999 (DAMAGE_CAP + MIN_DAMAGE in
|
|
-- BattleCommand_DamageCalc).
|
|
Damage.MAX_DAMAGE = 999
|
|
|
|
-- BattleCommand_DamageCalc's tail (engine/battle/effect_commands.asm) caps the
|
|
-- computed damage at DAMAGE_CAP (997) and then adds MIN_DAMAGE (2) back, so
|
|
-- every damaging hit leaves DamageCalc worth at least 2 -- which is what keeps
|
|
-- a resisted hit from flooring to zero once the type matchup halves it.
|
|
Damage.MIN_DAMAGE = 2
|
|
|
|
-- Stat stage multipliers (numerator, denominator), -6..+6. Same table as
|
|
-- Gen 1; a critical hit skips only the negative half for the attacker.
|
|
local STAGE = {
|
|
[-6] = { 25, 100 }, [-5] = { 28, 100 }, [-4] = { 33, 100 },
|
|
[-3] = { 40, 100 }, [-2] = { 50, 100 }, [-1] = { 66, 100 },
|
|
[0] = { 1, 1 },
|
|
[1] = { 15, 10 }, [2] = { 2, 1 }, [3] = { 25, 10 },
|
|
[4] = { 3, 1 }, [5] = { 35, 10 }, [6] = { 4, 1 },
|
|
}
|
|
|
|
function Damage.stageMultiplier(stage)
|
|
local entry = STAGE[math.max(-6, math.min(6, stage or 0))]
|
|
return entry[1], entry[2]
|
|
end
|
|
|
|
-- Apply a stat stage, flooring like the cart's Multiply/Divide pair, and never
|
|
-- letting a stat reach 0 (a 0 defence would divide by zero).
|
|
function Damage.applyStage(value, stage)
|
|
local numerator, denominator = Damage.stageMultiplier(stage)
|
|
local out = math.floor(value * numerator / denominator)
|
|
return math.max(1, out)
|
|
end
|
|
|
|
-- Is this move physical? `types` is type_chart.lua's `types` table.
|
|
function Damage.isPhysical(moveType, types)
|
|
local record = types and types[moveType]
|
|
if record and record.category then return record.category == "physical" end
|
|
-- Without the table, fall back to the Gen 1/2 boundary: the physical block
|
|
-- runs NORMAL..GROUND, and the special block starts at FIRE.
|
|
local PHYSICAL = {
|
|
NORMAL = true, FIGHTING = true, FLYING = true, POISON = true,
|
|
GROUND = true, ROCK = true, BUG = true, GHOST = true, STEEL = true,
|
|
}
|
|
return PHYSICAL[moveType] == true
|
|
end
|
|
|
|
-- The 1-in-N chance for a critical level.
|
|
function Damage.criticalChance(level)
|
|
local capped = math.max(0, math.min(6, level or 0))
|
|
return Damage.CRITICAL_CHANCES[capped]
|
|
end
|
|
|
|
-- BattleCommand_Critical, as a level rather than a roll:
|
|
-- +1 Focus Energy, +2 a high-crit move, +1 Scope Lens,
|
|
-- +2 Lucky Punch on Chansey / Stick on Farfetch'd.
|
|
function Damage.criticalLevel(opts)
|
|
local level = 0
|
|
if opts.focusEnergy then level = level + 1 end
|
|
if opts.highCritMove then level = level + 2 end
|
|
if opts.scopeLens then level = level + 1 end
|
|
if opts.speciesItemBonus then level = level + 2 end
|
|
return math.min(6, level)
|
|
end
|
|
|
|
-- Roll a critical hit. `random(n)` must return 0..n-1 (the cart compares a
|
|
-- BattleRandom byte against the chance), and defaults to love/math random.
|
|
function Damage.rollCritical(criticalLevel, random)
|
|
local chance = Damage.criticalChance(criticalLevel)
|
|
local roll
|
|
if random then
|
|
roll = random(chance)
|
|
elseif love and love.math then
|
|
roll = love.math.random(chance) - 1
|
|
else
|
|
roll = math.random(chance) - 1
|
|
end
|
|
return roll == 0
|
|
end
|
|
|
|
-- The x10 type multiplier of a move against a defender, applying each matchup
|
|
-- row separately and flooring in between -- the same rule Gen 1 follows, which
|
|
-- is why a dual type can land on 4x or 0.25x.
|
|
function Damage.typeMultiplier(moveType, defenderTypes, matchups)
|
|
local multiplier = 10
|
|
for _, row in ipairs(matchups or {}) do
|
|
if row.attacker == moveType then
|
|
for _, defenderType in ipairs(defenderTypes or {}) do
|
|
if row.defender == defenderType then
|
|
multiplier = math.floor(multiplier * row.multiplier / 10)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return multiplier
|
|
end
|
|
|
|
-- The core formula (BattleCommand_DamageCalc):
|
|
--
|
|
-- (((2 * Level / 5 + 2) * Power * Attack / Defense) / 50)
|
|
--
|
|
-- every step floored, defence clamped to at least 1.
|
|
function Damage.base(level, power, attack, defense)
|
|
if (power or 0) <= 0 then return 0 end
|
|
defense = math.max(1, defense or 1)
|
|
local value = math.floor(level * 2 / 5) + 2
|
|
value = value * power
|
|
value = value * attack
|
|
value = math.floor(value / defense)
|
|
value = math.floor(value / 50)
|
|
return value
|
|
end
|
|
|
|
-- Every `info` table Damage.calc returns carries BOTH generations' names for
|
|
-- the same two facts: Gen 2 calls them `critical` and `effectiveness`, Gen 1
|
|
-- (src/battle/Damage.lua) calls them `crit` and `typeMult`. The battle.damage
|
|
-- hook hands this table to whatever wrapped it, and a mod written against Red
|
|
-- must be able to read what it wrapped.
|
|
local function withGen1Names(info)
|
|
info.crit = info.critical
|
|
info.typeMult = info.effectiveness
|
|
return info
|
|
end
|
|
|
|
-- opts:
|
|
-- level, power, moveType
|
|
-- attacker { attack, specialAttack, types, stages = { attack =, ... } }
|
|
-- defender { defense, specialDefense, types, stages }
|
|
-- types, matchups -- type_chart.lua's `types` / `matchups`
|
|
-- critical -- boolean (roll it with rollCritical first)
|
|
-- itemBoostPercent -- type-boost held item, e.g. 10 for Charcoal
|
|
-- weatherPercent -- DoWeatherModifiers in tenths (15 / 5 / nil),
|
|
-- applied ahead of the badge boost and STAB
|
|
-- badgeTypeBoost -- DoBadgeTypeBoosts: the player owns the badge
|
|
-- matching the move's type, +1/8 before STAB
|
|
-- variation -- 85..100; omit to roll
|
|
-- random(n) -- 0..n-1, for the variation roll
|
|
-- screen -- Reflect/Light Screen active on the defender
|
|
-- defenseHalved -- EFFECT_SELFDESTRUCT's srl c
|
|
--
|
|
-- Returns damage, info where info carries the pieces a battle message needs:
|
|
-- effectiveness (x10), critical, physical, variation.
|
|
function Damage.calc(opts)
|
|
local physical = Damage.isPhysical(opts.moveType, opts.types)
|
|
local attacker = opts.attacker or {}
|
|
local defender = opts.defender or {}
|
|
local stagesA = attacker.stages or {}
|
|
local stagesD = defender.stages or {}
|
|
|
|
local rawAttack = physical and (attacker.attack or 1)
|
|
or (attacker.specialAttack or attacker.special or 1)
|
|
local rawDefense = physical and (defender.defense or 1)
|
|
or (defender.specialDefense or defender.special or 1)
|
|
local stageA = physical and (stagesA.attack or 0)
|
|
or (stagesA.specialAttack or 0)
|
|
local stageD = physical and (stagesD.defense or 0)
|
|
or (stagesD.specialDefense or 0)
|
|
|
|
-- A critical hit ignores stat changes that would *lower* the damage: the
|
|
-- attacker's negative stages and the defender's positive ones.
|
|
if opts.critical then
|
|
if stageA < 0 then stageA = 0 end
|
|
if stageD > 0 then stageD = 0 end
|
|
end
|
|
|
|
local attack = Damage.applyStage(rawAttack, stageA)
|
|
local defense = Damage.applyStage(rawDefense, stageD)
|
|
|
|
-- Reflect and Light Screen double the matching defence, and are the one
|
|
-- multiplier a critical hit also ignores.
|
|
if opts.screen and not opts.critical then
|
|
defense = defense * 2
|
|
end
|
|
|
|
-- BattleCommand_DamageCalc (effect_commands.asm:2905-2913): Selfdestruct and
|
|
-- Explosion halve the defence, never below 1.
|
|
if opts.defenseHalved then defense = math.max(1, math.floor(defense / 2)) end
|
|
|
|
if (opts.power or 0) <= 0 then
|
|
return 0, withGen1Names({ effectiveness = 10, critical = false,
|
|
physical = physical })
|
|
end
|
|
local damage = Damage.base(opts.level or 1, opts.power or 0, attack, defense)
|
|
|
|
-- Type-boost held items multiply before the crit (.NextItem / .DoneItem).
|
|
if opts.itemBoostPercent and opts.itemBoostPercent > 0 then
|
|
damage = math.floor(damage * (100 + opts.itemBoostPercent) / 100)
|
|
end
|
|
|
|
if opts.critical then damage = damage * 2 end
|
|
|
|
-- BattleCommand_DamageCalc's tail: cap at DAMAGE_CAP, then add MIN_DAMAGE
|
|
-- back, so even a hit whose stat math floored to nothing leaves with 2.
|
|
damage = math.min(damage, Damage.MAX_DAMAGE - Damage.MIN_DAMAGE)
|
|
+ Damage.MIN_DAMAGE
|
|
|
|
-- DoWeatherModifiers (engine/battle/misc.asm:102-140), farcalled by
|
|
-- BattleCommand_Stab as its very FIRST act (effect_commands.asm:1254): it
|
|
-- sits ahead of the badge boost, the STAB x1.5, the type rows and
|
|
-- DamageVariation, so every later step floors on top of it. The table's
|
|
-- values are tenths (weather_modifiers.asm: MORE_EFFECTIVE 15,
|
|
-- NOT_VERY_EFFECTIVE 05), and .ApplyModifier's zero-quotient arm forces the
|
|
-- result back to 1, so a weather-halved hit never falls to nothing.
|
|
if opts.weatherPercent and opts.weatherPercent ~= 10 then
|
|
damage = math.max(1, math.floor(damage * opts.weatherPercent / 10))
|
|
end
|
|
|
|
-- DoBadgeTypeBoosts (engine/battle/misc.asm:146), farcalled from
|
|
-- BattleCommand_Stab ahead of the STAB multiply: a matching owned badge
|
|
-- adds an eighth of the running damage, at least 1, on the player's turn.
|
|
if opts.badgeTypeBoost then
|
|
damage = damage + math.max(1, math.floor(damage / 8))
|
|
end
|
|
|
|
-- STAB, then each type row. BattleCommand_Stab does STAB first, so a
|
|
-- resisted same-type move floors after the x1.5.
|
|
local stab = false
|
|
for _, attackerType in ipairs(attacker.types or {}) do
|
|
if attackerType == opts.moveType then stab = true break end
|
|
end
|
|
if stab then damage = math.floor(damage * 15 / 10) end
|
|
|
|
-- Each matchup row multiplies the running damage separately, the way
|
|
-- BattleCommand_Stab's .TypesLoop does -- and its zero-quotient check forces
|
|
-- the damage back to 1 whenever a non-immune row floors it to nothing, so a
|
|
-- resisted hit that lands always deals at least 1 HP.
|
|
local effectiveness = Damage.typeMultiplier(
|
|
opts.moveType, defender.types, opts.matchups)
|
|
for _, row in ipairs(opts.matchups or {}) do
|
|
if row.attacker == opts.moveType then
|
|
for _, defenderType in ipairs(defender.types or {}) do
|
|
if row.defender == defenderType then
|
|
damage = math.floor(damage * row.multiplier / 10)
|
|
if damage == 0 and row.multiplier > 0 then damage = 1 end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if effectiveness <= 0 or damage <= 0 then
|
|
return 0, withGen1Names({
|
|
effectiveness = effectiveness, critical = opts.critical or false,
|
|
physical = physical, stab = stab,
|
|
})
|
|
end
|
|
|
|
-- Damage variation last, and only when the running damage is 2 or more
|
|
-- (BattleCommand_DamageVariation returns early below that).
|
|
local variation = opts.variation
|
|
if not variation then
|
|
if opts.random then
|
|
variation = Damage.MIN_VARIATION
|
|
+ opts.random(Damage.MAX_VARIATION - Damage.MIN_VARIATION + 1)
|
|
elseif love and love.math then
|
|
variation = love.math.random(Damage.MIN_VARIATION, Damage.MAX_VARIATION)
|
|
else
|
|
variation = math.random(Damage.MIN_VARIATION, Damage.MAX_VARIATION)
|
|
end
|
|
end
|
|
if damage >= 2 then
|
|
damage = math.floor(damage * variation / 100)
|
|
end
|
|
|
|
damage = math.max(1, math.min(Damage.MAX_DAMAGE, damage))
|
|
return damage, withGen1Names({
|
|
effectiveness = effectiveness,
|
|
critical = opts.critical or false,
|
|
physical = physical,
|
|
stab = stab,
|
|
variation = variation,
|
|
})
|
|
end
|
|
|
|
-- Accuracy check. Gen 2 rolls one byte against accuracy scaled by the
|
|
-- attacker's accuracy stage and the defender's evasion stage; accuracy of 0 in
|
|
-- the data means "never misses" (Swift and friends).
|
|
function Damage.rollHit(accuracy, accuracyStage, evasionStage, random)
|
|
if not accuracy or accuracy <= 0 then return true end
|
|
local numerator, denominator = Damage.stageMultiplier(accuracyStage or 0)
|
|
local value = math.floor(accuracy * numerator / denominator)
|
|
numerator, denominator = Damage.stageMultiplier(-(evasionStage or 0))
|
|
value = math.floor(value * numerator / denominator)
|
|
value = math.max(1, math.min(100, value))
|
|
local roll
|
|
if random then
|
|
roll = random(100)
|
|
elseif love and love.math then
|
|
roll = love.math.random(100) - 1
|
|
else
|
|
roll = math.random(100) - 1
|
|
end
|
|
return roll < value
|
|
end
|
|
|
|
return Damage
|