mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
CLOSES #1396, CLOSES #1398, CLOSES #1400, CLOSES #1401, CLOSES #1406, CLOSES #1407, CLOSES #1411, CLOSES #1413, CLOSES #1415, CLOSES #1416, CLOSES #1417, CLOSES #1419, CLOSES #1421, CLOSES #1422, CLOSES #1423, CLOSES #1424, CLOSES #1425, CLOSES #1427, CLOSES #1428, CLOSES #1429, CLOSES #1431, CLOSES #1432, CLOSES #1433, CLOSES #1435, CLOSES #1437, CLOSES #1440, CLOSES #1441, CLOSES #1442, CLOSES #1443, CLOSES #1444, CLOSES #1447, CLOSES #1449, CLOSES #1456, CLOSES #1461, CLOSES #1464, CLOSES #1465, CLOSES #1466, CLOSES #1468, CLOSES #1469, CLOSES #1470
This commit is contained in:
@@ -4111,8 +4111,8 @@ function BattleState:awardExp()
|
||||
end
|
||||
local function applyShare(mon, split, announce)
|
||||
local playerId = self.game.save.player and self.game.save.player.id
|
||||
local traded = mon.otId ~= nil and playerId ~= nil
|
||||
and mon.otId ~= playerId or mon.traded == true and mon.otId == nil
|
||||
local traded = mon.traded == true
|
||||
or (mon.otId ~= nil and playerId ~= nil and mon.otId ~= playerId)
|
||||
local levels, gained = Experience.apply(self.data, mon, self.enemy.def,
|
||||
self.enemy.mon.level, self.kind == "trainer",
|
||||
split, traded)
|
||||
|
||||
@@ -1868,7 +1868,11 @@ function Battle:useMove(attacker, defender, moveId)
|
||||
if status and (def.power or 0) == 0 then
|
||||
-- A refused primary status is a failed move (effect_commands.asm:3748,
|
||||
-- :6656); a refused secondary already animated and stays unmarked (:3752).
|
||||
if Battle.AI_FAIL_STATUSES[status]
|
||||
if self:statusRefusedByType(defender, def.type, status) then
|
||||
self:markMissed()
|
||||
self:emit({ kind = "message",
|
||||
text = "It doesn't affect " .. self:monName(defender) .. "..." })
|
||||
elseif Battle.AI_FAIL_STATUSES[status]
|
||||
and self:aiRandomFail(attacker, defender) then
|
||||
self:markMissed()
|
||||
self:emit({ kind = "message", text = "But it failed!" })
|
||||
@@ -1880,7 +1884,8 @@ function Battle:useMove(attacker, defender, moveId)
|
||||
and record.status or nil
|
||||
-- engine/battle/effect_commands.asm:6325
|
||||
if secondary and (defender.hp or 0) > 0
|
||||
and not self:safeguarded(defender) then
|
||||
and not self:safeguarded(defender)
|
||||
and not self:statusRefusedByType(defender, def.type, secondary) then
|
||||
local chance = def.effectChance or 0
|
||||
if chance > 0 and rand(self.random, 100) < chance then
|
||||
self:applyStatus(defender, secondary, attacker)
|
||||
@@ -2941,6 +2946,27 @@ end
|
||||
|
||||
-- `source` is the battler that inflicted it, carried only so
|
||||
-- battle.status_inflicted can name it the way Gen 1's does.
|
||||
-- BattleCommand_Paralyze and BattleCommand_Poison refuse on a zero matchup,
|
||||
-- and the poison pair also refuses a POISON-type target: effect_commands.asm
|
||||
-- :5788 (paralyze), :3671 (poison), :3646 / :4019 (the secondary arms).
|
||||
-- Sleep, confusion and stat changes are deliberately not gated.
|
||||
function Battle:statusRefusedByType(defender, moveType, status)
|
||||
if not (status == "paralyze" or status == "poison" or status == "toxic") then
|
||||
return false
|
||||
end
|
||||
local types = (self:speciesDef(defender) or {}).types or defender.types or {}
|
||||
if moveType then
|
||||
local matchups = self.data.type_chart and self.data.type_chart.matchups
|
||||
if Damage.typeMultiplier(moveType, types, matchups) == 0 then return true end
|
||||
end
|
||||
if status == "poison" or status == "toxic" then
|
||||
for _, t in ipairs(types) do
|
||||
if t == "POISON" then return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Battle:applyStatus(mon, status, source)
|
||||
if (mon.hp or 0) <= 0 then return false end
|
||||
-- Confusion is SUBSTATUS_CONFUSED on the cart, not a status byte: it lives
|
||||
@@ -3202,6 +3228,7 @@ end
|
||||
-- player's own.
|
||||
function Battle:isOutsider(mon)
|
||||
local playerId = self.save and self.save.player and self.save.player.id
|
||||
if mon.traded == true then return true end
|
||||
if mon.otId == nil or playerId == nil then return false end
|
||||
return mon.otId ~= playerId
|
||||
end
|
||||
|
||||
@@ -388,7 +388,8 @@ function Mon.stampOT(save, mon)
|
||||
mon.ot = mon.ot or player.name
|
||||
-- NpcTrade.lua:150: `ot` is what Breeding reads, `otName` what the summary prints.
|
||||
mon.otName = mon.otName or mon.ot
|
||||
mon.otId = mon.otId or player.id
|
||||
-- engine/battle/experience.asm:69: a traded mon keeps its own OT id
|
||||
if not mon.traded then mon.otId = mon.otId or player.id end
|
||||
return mon
|
||||
end
|
||||
|
||||
|
||||
@@ -1233,6 +1233,7 @@ function Game:restoreSave(loaded, recovered, opts)
|
||||
self:applyOptions(loaded.options)
|
||||
-- saves from before OT/ID stamping: backfill with the player's (after
|
||||
-- the scrub, so every mon the stamp loop sees is known)
|
||||
SaveData.repairTradedOtIds(loaded)
|
||||
local stamp = require("src.battle.BattleState").stampOT
|
||||
for _, mon in ipairs(loaded.party or {}) do stamp(loaded, mon) end
|
||||
for _, box in ipairs(loaded.boxes or {}) do
|
||||
|
||||
@@ -1923,6 +1923,26 @@ function SaveData.applyPostGameHome(save, boot)
|
||||
return heal
|
||||
end
|
||||
|
||||
-- 0.1.82-0.1.9x loads stamped the player's own id onto traded mons and saved
|
||||
-- it, which reads back as home-caught. A caught mon can never carry
|
||||
-- traded=true, so clearing that pair is safe on every load. #1461
|
||||
function SaveData.repairTradedOtIds(save)
|
||||
local playerId = save and save.player and save.player.id
|
||||
if playerId == nil then return 0 end
|
||||
local fixed = 0
|
||||
local function scrub(mon)
|
||||
if mon and mon.traded == true and mon.otId == playerId then
|
||||
mon.otId = nil
|
||||
fixed = fixed + 1
|
||||
end
|
||||
end
|
||||
for _, mon in ipairs(save.party or {}) do scrub(mon) end
|
||||
for _, box in ipairs(save.boxes or {}) do
|
||||
for _, mon in ipairs(box) do scrub(mon) end
|
||||
end
|
||||
return fixed
|
||||
end
|
||||
|
||||
-- Softlocked 0.1.11 saves: still standing in HALL_OF_FAME after credits,
|
||||
-- with lastOutdoor on Indigo. One-shot rescue on CONTINUE.
|
||||
function SaveData.needsPostGameRescue(save)
|
||||
|
||||
@@ -46,4 +46,30 @@ do
|
||||
T.eq(tradedWithId.otId, 777, "a recorded foreign OT id is never overwritten")
|
||||
end
|
||||
|
||||
-- #1461: saves that passed through 0.1.82-0.1.9x already have the player's
|
||||
-- own id written onto traded mons, so stamping correctly from now on does
|
||||
-- not help them. The repair runs on every load, not behind a format gate.
|
||||
do
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local save = newSave()
|
||||
local poisoned = { traded = true, otId = 12345 }
|
||||
local foreign = { traded = true, otId = 777 }
|
||||
local caught = { otId = 12345 }
|
||||
save.party = { poisoned, foreign, caught }
|
||||
save.boxes = { { { traded = true, otId = 12345 } } }
|
||||
|
||||
T.eq(SaveData.repairTradedOtIds(save), 2, "both poisoned mons are repaired")
|
||||
T.eq(poisoned.otId, nil, "a traded mon carrying the player id is cleared")
|
||||
T.eq(save.boxes[1][1].otId, nil, "boxed mons are repaired too")
|
||||
T.eq(foreign.otId, 777, "a real foreign OT id is left alone")
|
||||
T.eq(caught.otId, 12345, "a caught mon keeps the player id")
|
||||
T.eq(caught.traded, nil, "and is never marked traded")
|
||||
|
||||
T.eq(SaveData.repairTradedOtIds(save), 0, "the repair is idempotent")
|
||||
|
||||
-- and after the repair the stamp loop must not re-adopt it
|
||||
BattleState.stampOT(save, poisoned)
|
||||
T.eq(poisoned.otId, nil, "the stamp loop does not undo the repair")
|
||||
end
|
||||
|
||||
T.finish("exp traded ot survives reload bug 1265")
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
-- engine/battle/effect_commands.asm:5788, :3671, :3646, :4019
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
love = require("tests.love_stub")
|
||||
|
||||
local T = require("tests.harness")
|
||||
local Battle = require("src.battle.gen2.Battle")
|
||||
|
||||
local MATCHUPS = {
|
||||
{ attacker = "ELECTRIC", defender = "GROUND", multiplier = 0 },
|
||||
{ attacker = "POISON", defender = "STEEL", multiplier = 0 },
|
||||
{ attacker = "POISON", defender = "POISON", multiplier = 5 },
|
||||
{ attacker = "NORMAL", defender = "GHOST", multiplier = 0 },
|
||||
{ attacker = "PSYCHIC_TYPE", defender = "GROUND", multiplier = 10 },
|
||||
}
|
||||
|
||||
local POKEMON = {
|
||||
GEODUDE = { types = { "ROCK", "GROUND" } },
|
||||
MAGNEMITE = { types = { "STEEL", "ELECTRIC" } },
|
||||
ZUBAT = { types = { "POISON", "FLYING" } },
|
||||
GASTLY = { types = { "GHOST", "POISON" } },
|
||||
PIDGEY = { types = { "NORMAL", "FLYING" } },
|
||||
}
|
||||
|
||||
local battle = setmetatable({
|
||||
data = { type_chart = { matchups = MATCHUPS }, pokemon = POKEMON },
|
||||
}, { __index = Battle })
|
||||
|
||||
local function mon(species) return { species = species, hp = 20 } end
|
||||
local function refused(species, moveType, status)
|
||||
return battle:statusRefusedByType(mon(species), moveType, status)
|
||||
end
|
||||
|
||||
-- The two repros the reporter filed, neither of which the #1318 fix touched.
|
||||
T.eq(refused("GEODUDE", "ELECTRIC", "paralyze"), true,
|
||||
"Thunder Wave does not affect a GROUND type")
|
||||
T.eq(refused("MAGNEMITE", "POISON", "poison"), true,
|
||||
"Poison Gas does not affect a STEEL type")
|
||||
|
||||
-- Poison vs POISON is 0.5x, not an immunity, so only CheckIfTargetIsPoisonType
|
||||
-- refuses it: this is the Poison Sting on Zubat case.
|
||||
T.eq(refused("ZUBAT", "POISON", "poison"), true,
|
||||
"a POISON type cannot be poisoned even on a resisted, landed hit")
|
||||
T.eq(refused("GASTLY", "POISON", "toxic"), true,
|
||||
"Toxic is refused by the same check")
|
||||
T.eq(refused("GASTLY", "NORMAL", "paralyze"), true,
|
||||
"Glare does not affect a GHOST type")
|
||||
|
||||
-- Everything the cart deliberately leaves ungated stays ungated.
|
||||
T.eq(refused("GEODUDE", "PSYCHIC_TYPE", "sleep"), false,
|
||||
"Hypnosis still lands on a GROUND type")
|
||||
T.eq(refused("GEODUDE", "PSYCHIC_TYPE", "confuse"), false,
|
||||
"confusion is never type-gated")
|
||||
T.eq(refused("PIDGEY", "ELECTRIC", "paralyze"), false,
|
||||
"Thunder Wave still paralyses a non-immune target")
|
||||
T.eq(refused("PIDGEY", "POISON", "poison"), false,
|
||||
"a non-POISON target is still poisonable")
|
||||
T.eq(refused("GEODUDE", nil, "paralyze"), false,
|
||||
"a status with no move type behind it is not refused")
|
||||
|
||||
T.finish("gen2 status type immunity bug 1444")
|
||||
@@ -132,6 +132,33 @@ do
|
||||
check(cat.species[1] < cat.species[2], "species sorted")
|
||||
end
|
||||
|
||||
-- RomExtractorGen2 stamps generation/source beside the id-keyed records, and
|
||||
-- they sort last because lowercase follows uppercase. #1466
|
||||
do
|
||||
local gold = {
|
||||
pokemon = { generation = 2, source = "ROM", CHIKORITA = { name = "CHIKORITA" } },
|
||||
items = { generation = 2, source = "ROM", POTION = { name = "POTION" } },
|
||||
moves = {
|
||||
generation = 2, source = "ROM:Moves + MoveNames",
|
||||
TACKLE = { pp = 35 }, ZAP_CANNON = { pp = 5 },
|
||||
},
|
||||
}
|
||||
local cat = Catalog.build(gold)
|
||||
eq(#cat.moves, 2, "gold move catalog holds only real moves")
|
||||
eq(cat.moves[#cat.moves], "ZAP_CANNON", "and the last entry is a move, not a scalar")
|
||||
for _, list in pairs(cat) do
|
||||
for _, id in ipairs(list) do
|
||||
check(id ~= "generation" and id ~= "source",
|
||||
"no provenance scalar reached a catalog: " .. tostring(id))
|
||||
end
|
||||
end
|
||||
|
||||
local S = { data = gold, cat = cat }
|
||||
local mon = { moves = { { id = "ZAP_CANNON", pp = 5 } } }
|
||||
check(require("Ops").cycleMove(S, mon, 1), "cycling off the last move succeeds")
|
||||
eq(mon.moves[1].id, "TACKLE", "and wraps to the first move instead of a scalar")
|
||||
end
|
||||
|
||||
do
|
||||
local events = Catalog.scrapeEvents("data/scripts", "data/generated/trainer_headers.lua")
|
||||
check(#events > 50, "scraped events")
|
||||
|
||||
@@ -9,11 +9,23 @@ local function sortedKeys(t)
|
||||
return keys
|
||||
end
|
||||
|
||||
-- Gen 2's generated tables carry provenance scalars (generation, source)
|
||||
-- beside the id-keyed records, so a wheel built from every key would offer
|
||||
-- them as pickable entries. #1466
|
||||
local function sortedRecordKeys(t)
|
||||
local keys = {}
|
||||
for k, v in pairs(t or {}) do
|
||||
if type(v) == "table" then table.insert(keys, k) end
|
||||
end
|
||||
table.sort(keys)
|
||||
return keys
|
||||
end
|
||||
|
||||
function Catalog.build(data)
|
||||
return {
|
||||
species = sortedKeys(data.pokemon),
|
||||
items = sortedKeys(data.items),
|
||||
moves = sortedKeys(data.moves),
|
||||
species = sortedRecordKeys(data.pokemon),
|
||||
items = sortedRecordKeys(data.items),
|
||||
moves = sortedRecordKeys(data.moves),
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ end
|
||||
function MonOps.setMove(data, mon, slot, moveId)
|
||||
assert(slot >= 1 and slot <= 4)
|
||||
local mdef = data.moves[moveId]
|
||||
assert(mdef, "unknown move")
|
||||
assert(type(mdef) == "table", "unknown move")
|
||||
mon.moves = mon.moves or {}
|
||||
mon.moves[slot] = {
|
||||
id = moveId,
|
||||
|
||||
@@ -441,7 +441,7 @@ function Ops.cycleMove(S, mon, slot)
|
||||
end
|
||||
for step = 1, #moves do
|
||||
local nextId = moves[((idx + step - 1) % #moves) + 1]
|
||||
if S.data and S.data.moves and S.data.moves[nextId] then
|
||||
if S.data and S.data.moves and type(S.data.moves[nextId]) == "table" then
|
||||
MonOps.setMove(S.data, mon, slot, nextId)
|
||||
return Ops.mark(S, ("Move %d set to %s"):format(slot, nextId))
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user