mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
@@ -579,7 +579,7 @@ local function loadSheet(tileName)
|
||||
end
|
||||
|
||||
--- Draw active door animation overlay
|
||||
function Doors.draw(camX, camY)
|
||||
function Doors.draw(camX, camY, canvasW, canvasH)
|
||||
local anim = Doors._activeAnim
|
||||
if not anim then return end
|
||||
if not (love and love.graphics and love.graphics.rectangle) then return end
|
||||
@@ -588,20 +588,23 @@ function Doors.draw(camX, camY)
|
||||
local sx = anim.x * CELL - (camX or 0)
|
||||
local sy = anim.y * CELL - (camY or 0)
|
||||
|
||||
-- Viewport bounds check
|
||||
if sx < -CELL or sy < -32 or sx > 256 or sy > 176 then
|
||||
return
|
||||
end
|
||||
|
||||
-- src/field_door.c:457
|
||||
local tileName = anim.tile
|
||||
if not tileName then return end
|
||||
|
||||
local sheet = loadSheet(tileName)
|
||||
local hasSheet = sheet and sheet.image and sheet.quads
|
||||
local width = hasSheet and sheet.frame_width or CELL
|
||||
local height = hasSheet and sheet.frame_height or CELL
|
||||
local yOffset = (height > CELL) and CELL or 0
|
||||
local top = sy - yOffset
|
||||
if sx + width <= 0 or top + height <= 0
|
||||
or sx >= (canvasW or 240) or top >= (canvasH or 160) then
|
||||
return
|
||||
end
|
||||
|
||||
if sheet and sheet.image and sheet.quads then
|
||||
if hasSheet then
|
||||
local frame = math.min(anim.frame, sheet.frames - 1)
|
||||
local yOffset = (sheet.frame_height > 16) and 16 or 0
|
||||
|
||||
-- Authentic black interior background behind the door graphic
|
||||
love.graphics.setColor(0.05, 0.07, 0.1, 1)
|
||||
@@ -665,4 +668,3 @@ end
|
||||
return Doors
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -234,11 +234,21 @@ local function bg_event_at(game, fx, fy, elevation, facingDir)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function hidden_item_store(session)
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local Space = package.loaded["src.core.game3.scripting.space"]
|
||||
if session and Runtime and Runtime.getSession and Runtime.getSession() == session
|
||||
and Space and Space.store then
|
||||
return Space.store, Space
|
||||
end
|
||||
return session and (session.store or session)
|
||||
end
|
||||
|
||||
local function hidden_item_at(game, x, y, elevation)
|
||||
local session = Field._session
|
||||
local events = get_map_bg_events(game)
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local store = session and (session.store or session)
|
||||
local store = hidden_item_store(session)
|
||||
for _, ev in ipairs(events) do
|
||||
if (ev.type == "hidden_item" or ev.kind == 7) and ev.x == x and ev.y == y then
|
||||
local flag = ev.flag or (ev.hiddenItemId and (0x3E8 + ev.hiddenItemId))
|
||||
@@ -263,7 +273,7 @@ function Field.pickUpHiddenItem(game, hidden)
|
||||
local ItemsData = require("src.core.game3.items_data")
|
||||
local Message = require("src.ui.game3.message")
|
||||
local Audio = require("src.core.game3.audio")
|
||||
local store = session and (session.store or session)
|
||||
local store, Space = hidden_item_store(session)
|
||||
|
||||
local flag = hidden.flag or (hidden.hiddenItemId and (0x3E8 + hidden.hiddenItemId))
|
||||
if flag and Flags.getFlag(store, nil, flag) then
|
||||
@@ -274,7 +284,7 @@ function Field.pickUpHiddenItem(game, hidden)
|
||||
local qty = hidden.quantity or 1
|
||||
local bag = session and session.bag
|
||||
|
||||
if bag and not Bag.canAdd(bag, itemId, qty) then
|
||||
if not bag or not Bag.add(bag, itemId, qty) then
|
||||
Message.show("Too bad!\nThe BAG is full…", {
|
||||
done = function()
|
||||
Message.close()
|
||||
@@ -283,11 +293,9 @@ function Field.pickUpHiddenItem(game, hidden)
|
||||
return true
|
||||
end
|
||||
|
||||
if bag then
|
||||
Bag.add(bag, itemId, qty)
|
||||
end
|
||||
if flag and store then
|
||||
Flags.setFlag(store, nil, flag, true)
|
||||
if Space then Space.persistSession(nil, game or Field._game) end
|
||||
end
|
||||
|
||||
Audio.playFanfare(257)
|
||||
@@ -323,7 +331,7 @@ function Field.useItemfinder(session, showOWMessage)
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Audio = require("src.core.game3.audio")
|
||||
local Message = require("src.ui.game3.message")
|
||||
local store = session and (session.store or session)
|
||||
local store = hidden_item_store(session)
|
||||
|
||||
local found = nil
|
||||
local underfoot = false
|
||||
|
||||
@@ -1071,7 +1071,7 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
do
|
||||
local okDoors, Doors = pcall(require, "src.core.game3.doors")
|
||||
if okDoors and Doors and Doors.draw then
|
||||
Doors.draw(camX, camY)
|
||||
Doors.draw(camX, camY, canvasW, canvasH)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.normalize(mon)
|
||||
if type(mon) ~= "table" then return mon end
|
||||
local species = Pokemon.speciesOf(mon)
|
||||
if not species and tonumber(mon.speciesId) then
|
||||
species = Pokemon.speciesOf({ species = mon.speciesId, speciesNumbering = mon.speciesNumbering })
|
||||
end
|
||||
if not species or not Pokemon.isInternalSpecies(species) then return mon end
|
||||
mon.species, mon.speciesId = species, species
|
||||
mon.speciesNumbering = Pokemon.NUMBERING_INTERNAL
|
||||
local hp = tonumber(mon.hp)
|
||||
Pokemon.applyStats(mon)
|
||||
if hp then mon.hp = math.max(0, math.min(hp, mon.maxHp)) end
|
||||
mon.stats = mon.stats or {}
|
||||
for key, value in pairs({ hp = mon.maxHp, attack = mon.attack, defense = mon.defense,
|
||||
speed = mon.speed, spAtk = mon.spAtk, spDef = mon.spDef,
|
||||
specialAttack = mon.spAtk, specialDefense = mon.spDef }) do
|
||||
mon.stats[key] = value
|
||||
end
|
||||
for slot = 1, 4 do
|
||||
local move = mon.moves and mon.moves[slot]
|
||||
if type(move) == "table" then
|
||||
local id = tonumber(move.moveId or move.id or move.move)
|
||||
if not id and type(move.id) == "string" then
|
||||
for n = 1, 354 do
|
||||
if Pokemon.moveName(n) == move.id then id = n; break end
|
||||
end
|
||||
end
|
||||
if id then
|
||||
move.id, move.moveId = id, id
|
||||
mon.pp, mon.maxPp = mon.pp or {}, mon.maxPp or {}
|
||||
mon.pp[slot] = move.pp or mon.pp[slot] or Pokemon.movePp(id)
|
||||
mon.maxPp[slot] = move.maxPp or mon.maxPp[slot] or Pokemon.movePp(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
return mon
|
||||
end
|
||||
|
||||
function M.each(save, fn)
|
||||
for _, mon in pairs(save.party or {}) do fn(mon) end
|
||||
for _, box in pairs(save.storage and save.storage.boxes or {}) do
|
||||
for _, mon in pairs(type(box) == "table" and box.mons or {}) do fn(mon) end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -274,6 +274,7 @@ function Schema.fromSaveTable(save)
|
||||
modData = type(save.modData) == "table" and save.modData or {},
|
||||
meta = save.meta,
|
||||
}
|
||||
require("src.core.game3.save_mon").each(session, require("src.core.game3.save_mon").normalize)
|
||||
reset_state_on_continue(session)
|
||||
Schema.ensureMonBalls(session)
|
||||
Schema.repairOwnMons(session)
|
||||
|
||||
@@ -427,7 +427,8 @@ Natives.ALLOW = {
|
||||
adapters.startWildBattle(foe, function(result)
|
||||
local code = outcome_to_code(result)
|
||||
if ctx then ctx.lastBattleOutcome = code end
|
||||
setResult(ctx, code)
|
||||
-- pokefirered/src/battle_setup.c:458
|
||||
setResult(ctx, code == B_OUTCOME_WON and 0 or 1)
|
||||
if done then done() end
|
||||
end, { wildScripted = true })
|
||||
end)
|
||||
|
||||
@@ -94,8 +94,8 @@ local function parse_bg_events(rom, ptr, count)
|
||||
if kind == BG_EVENT_HIDDEN_ITEM then
|
||||
local item = rom:u16(base + 8)
|
||||
local info = rom:u16(base + 10)
|
||||
local hiddenItemId = info % 512
|
||||
local quantity = math.floor(info / 512) % 64
|
||||
local hiddenItemId = info % 256
|
||||
local quantity = math.floor(info / 256) % 128
|
||||
if quantity == 0 then quantity = 1 end
|
||||
local underfoot = info >= 32768
|
||||
local flag = FLAG_HIDDEN_ITEMS_START + hiddenItemId
|
||||
|
||||
@@ -27,7 +27,7 @@ Versions.ROM_SIZE = 16777216
|
||||
-- v99: gEggMoves → pokemon/egg_moves.lua (hidden-mon egg moves were inert).
|
||||
-- v100: location preview screens (sMapPreviewScreenData artwork) + ROM-derived
|
||||
-- mapsec names and sDungeonInfo dungeon descriptions.
|
||||
Versions.CACHE_VERSION = 110
|
||||
Versions.CACHE_VERSION = 111
|
||||
Versions.NATIVE_VERSION = 6
|
||||
Versions.OW_VERSION = 1
|
||||
Versions.ANIM_VERSION = 1
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_doors_viewport"
|
||||
local TARGETS = {
|
||||
{ label = "gym", x = 36, y = 10, tile = "SlidingDouble" },
|
||||
{ label = "wooden", x = 25, y = 11, tile = "Viridian" },
|
||||
}
|
||||
local VIEWS = {
|
||||
{ label = "portrait", w = 390, h = 844, zoom = 0 },
|
||||
{ label = "landscape_survey", w = 844, h = 390, zoom = -1 },
|
||||
}
|
||||
|
||||
return function(game)
|
||||
local failures = 0
|
||||
local function result(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
for _ = 1, 900 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(240)
|
||||
|
||||
local Map = require("src.core.game3.map")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Doors = require("src.core.game3.doors")
|
||||
local Dataset = require("src.core.game3.dataset")
|
||||
local Extract = require("src.import.gba.extract_island1")
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local FaithfulRes = require("src.core.FaithfulRes")
|
||||
local Zoom = require("src.render.Zoom")
|
||||
FaithfulRes.apply(0)
|
||||
require("src.render.Tilt").reset()
|
||||
require("src.render.ShaderFX").applyOptions({})
|
||||
require("src.core.ScreenPosition").setMode("center")
|
||||
Zoom.allowSurvey = true
|
||||
|
||||
local realDoorDraw, realDraw = Doors.draw, love.graphics.draw
|
||||
local insideDoor, observed = false, nil
|
||||
love.graphics.draw = function(...)
|
||||
local image, quad, x, y = ...
|
||||
if insideDoor then
|
||||
observed = { image = image, quad = quad, x = x, y = y, canvas = love.graphics.getCanvas() }
|
||||
end
|
||||
return realDraw(...)
|
||||
end
|
||||
Doors.draw = function(...)
|
||||
insideDoor = true
|
||||
realDoorDraw(...)
|
||||
insideDoor = false
|
||||
end
|
||||
|
||||
for _, view in ipairs(VIEWS) do
|
||||
Doors.reset()
|
||||
local _, _, flags = love.window.getMode()
|
||||
flags.fullscreen, flags.resizable = false, true
|
||||
flags.minwidth, flags.minheight = 1, 1
|
||||
assert(love.window.setMode(view.w, view.h, flags))
|
||||
Zoom.offset = view.zoom
|
||||
U.wait(15)
|
||||
local vw, vh = Renderer:worldViewSize()
|
||||
local ww, wh = love.graphics.getDimensions()
|
||||
print(string.format("[driver] %s window=%dx%d worldViewSize=%dx%d zoom=%d faithful=%s",
|
||||
view.label, ww, wh, vw, vh, Zoom.offset, tostring(FaithfulRes.scaleCap())))
|
||||
result(ww == view.w and wh == view.h, view.label .. "_window")
|
||||
result(view.label == "portrait" and vh > 400 or view.label == "landscape_survey" and vw > 600,
|
||||
view.label .. "_expanded_viewport")
|
||||
|
||||
for _, target in ipairs(TARGETS) do
|
||||
Doors.reset()
|
||||
Map.load(nil, game, "FR_VIRIDIAN_CITY", { x = target.x, y = target.y + 2, facing = "up" })
|
||||
Player.reset(target.x, target.y + 2, "up")
|
||||
if game.session then
|
||||
game.session.x, game.session.y, game.session.facing = target.x, target.y + 2, "up"
|
||||
end
|
||||
U.wait(60)
|
||||
local anim = Doors.open("FR_VIRIDIAN_CITY", target.x, target.y, { playSound = false })
|
||||
local label = view.label .. "_" .. target.label
|
||||
result(anim.tile == target.tile, label .. "_sheet")
|
||||
for _ = 1, 30 do
|
||||
if anim.frame == 1 then break end
|
||||
U.wait(1)
|
||||
end
|
||||
result(anim.frame == 1, label .. "_half_open")
|
||||
anim.mode = "hold"
|
||||
observed = nil
|
||||
local path = DIR .. "/" .. label .. "_half_open.png"
|
||||
local captured = U.shot(game, path)
|
||||
local sheet = Doors._sheets[target.tile]
|
||||
local drawn = observed and sheet and observed.image == sheet.image
|
||||
and observed.quad == sheet.quads[1] and observed.canvas == Renderer.worldCanvas
|
||||
result(drawn, label .. "_image_drawn")
|
||||
if drawn then
|
||||
result(observed.x > 256 or observed.y > 176, label .. "_past_old_cutoff")
|
||||
local info = Doors._manifest.doors[target.tile]
|
||||
local cache = Dataset.cache()
|
||||
local bytes = assert(cache:read(Extract.CACHE_ROOT .. "/doors/" .. info.file)
|
||||
or cache:read("doors/" .. info.file))
|
||||
local source = love.image.newImageData(info.width, info.height, "rgba8", bytes)
|
||||
local pixels = observed.canvas:newImageData()
|
||||
local matched, opaque, changed = 0, 0, 0
|
||||
for y = 0, info.frame_height - 1 do
|
||||
for x = 0, info.frame_width - 1 do
|
||||
local r, g, b, a = source:getPixel(x, y + info.frame_height)
|
||||
if a > 0.99 then
|
||||
opaque = opaque + 1
|
||||
local pr, pg, pb = pixels:getPixel(observed.x + x, observed.y + y)
|
||||
if math.abs(pr - r) < 0.01 and math.abs(pg - g) < 0.01 and math.abs(pb - b) < 0.01 then
|
||||
matched = matched + 1
|
||||
local cr, cg, cb, ca = source:getPixel(x, y)
|
||||
if ca < 0.99 or math.abs(cr-r) + math.abs(cg-g) + math.abs(cb-b) > 0.05 then
|
||||
changed = changed + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
print(string.format("[driver] %s opaque=%d matched=%d changed_from_closed=%d", label, opaque, matched, changed))
|
||||
result(opaque > 0 and matched == opaque and changed > 0, label .. "_half_open_pixels")
|
||||
source:release()
|
||||
pixels:release()
|
||||
end
|
||||
result(captured, label .. "_screenshot")
|
||||
end
|
||||
end
|
||||
Doors.draw, love.graphics.draw = realDoorDraw, realDraw
|
||||
Doors.reset()
|
||||
result(failures == 0, "doors_viewport_driver")
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,133 @@
|
||||
local U = require('tests.drivers.util')
|
||||
local DIR = os.getenv('POKEPORT_SHOT_DIR') or '/tmp/game3_editor_exp_roundtrip'
|
||||
|
||||
return function(game)
|
||||
local ok, err = xpcall(function()
|
||||
assert((os.getenv('POKEPORT_IDENTITY') or ''):match('^firered%-bsa'), 'isolated firered-bsa identity required')
|
||||
package.path = package.path .. ';./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua'
|
||||
local SaveData = require('src.core.SaveData')
|
||||
assert(not SaveData.isPortable(), 'driver requires isolated nonportable saves')
|
||||
for _ = 1, 900 do
|
||||
if game.phase == 'boot' and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
local slot = assert(SaveData.createSlot('firered'))
|
||||
SaveData.setActiveSlot('firered', slot)
|
||||
game:_handleBootAction({ action = 'new_game', name = 'RED' })
|
||||
U.wait(240)
|
||||
local Runtime = require('src.core.game3.runtime')
|
||||
local Party = require('src.core.game3.party')
|
||||
local P = require('src.core.game3.pokemon')
|
||||
local Growth = require('src.core.game3.summary_data')
|
||||
local Bag = require('src.core.game3.bag')
|
||||
local Storage = require('src.core.game3.storage')
|
||||
local App = require('tools.save-editor.App')
|
||||
local Ops = require('Ops')
|
||||
local Bridge = require('src.core.game3.battle_bridge')
|
||||
local Battle = require('src.core.game3.battle')
|
||||
local Anim = require('src.core.game3.battle.anim')
|
||||
local Ui = require('src.core.game3.battle.ui')
|
||||
local Summary = require('src.ui.game3.summary_menu')
|
||||
local BagMenu = require('src.ui.game3.bag_menu')
|
||||
local Boxes = require('src.ui.game3.box_storage_ui')
|
||||
local Pc = require('src.ui.game3.pc_menu')
|
||||
local function shot(name)
|
||||
U.wait(50)
|
||||
assert(U.shot(game, DIR .. '/' .. name .. '.png'), 'screenshot ' .. name)
|
||||
end
|
||||
local function continue()
|
||||
game:_handleBootAction({ action = 'continue' })
|
||||
U.wait(60)
|
||||
for _ = 1, 400 do
|
||||
if game.phase ~= 'quest_log' then break end
|
||||
U.tap(game, 'a'); U.wait(6)
|
||||
end
|
||||
U.wait(120)
|
||||
assert(game.phase == 'field', 'Continue did not reach field: ' .. tostring(game.phase))
|
||||
return assert(Runtime.getSession())
|
||||
end
|
||||
local session = assert(Runtime.getSession())
|
||||
session.party = {}
|
||||
Party.giveMon(session, 25, 20); Party.giveMon(session, 4, 20); Party.giveMon(session, 9, 20)
|
||||
session.storage = Storage.new()
|
||||
session.storage.boxes[1].mons[7] = table.remove(session.party, 3)
|
||||
session.storage.boxes[1].name, session.storage.boxes[1].wallpaper = 'KEPT', 8
|
||||
session.storage.items = { { id = 13, qty = 999 }, { id = 68, qty = 120 } }
|
||||
session.bag = Bag.new()
|
||||
for _, pair in ipairs({ { 13, 5 }, { 68, 2 }, { 4, 10 }, { 360, 1 }, { 289, 1 }, { 139, 3 } }) do
|
||||
assert(Bag.add(session.bag, pair[1], pair[2]))
|
||||
end
|
||||
session.party[1].custom = { sentinel = 'kept' }
|
||||
assert(game:saveGame())
|
||||
local path = assert(SaveData.slotDiskPath('firered', slot))
|
||||
App.load(path, { version = 'firered', slotId = slot, embedded = true })
|
||||
local S = App.getState()
|
||||
for _, mon in ipairs(S.save.party) do
|
||||
assert(Ops.setLevel(S, mon, 21))
|
||||
mon.exp = Growth.expForLevel(P.growthRate(mon.speciesId), 22) - 1
|
||||
assert(Ops.setMove(S, mon, 1, 10))
|
||||
end
|
||||
assert(Ops.addToBag(S, 'RARE_CANDY')); assert(Ops.bagAdjust(S, '68', 1))
|
||||
assert(Ops.pcAdjust(S, 'RARE_CANDY', -1))
|
||||
S.selectedParty, S.selectedBox, S.selectedBoxSlot = 2, 1, 2
|
||||
assert(Ops.deposit(S)); assert(S.save.storage.boxes[1].mons[7])
|
||||
assert(App.save()); App.unload()
|
||||
session = continue()
|
||||
assert(session.storage.boxes[1].mons[2].species == 4 and session.storage.boxes[1].mons[7].species == 9)
|
||||
print('PASS editor_sparse_deposit_continue')
|
||||
Boxes.show({ session = session }); Boxes.cursorSlot = 7
|
||||
shot('u2_editor_sparse_box_slots_2_7'); Boxes.close(); U.wait(10)
|
||||
App.load(path, { version = 'firered', slotId = slot, embedded = true })
|
||||
S = App.getState(); S.selectedBox, S.selectedBoxSlot = 1, 2
|
||||
assert(Ops.withdraw(S)); assert(App.save()); App.unload()
|
||||
session = continue()
|
||||
assert(session.storage.boxes[1].mons[2] == nil and session.storage.boxes[1].mons[7].species == 9)
|
||||
assert(#session.party == 2 and session.party[2].species == 4)
|
||||
print('PASS editor_sparse_withdraw_continue')
|
||||
for _, pair in ipairs({ { 13, 5 }, { 68, 4 }, { 4, 10 }, { 360, 1 }, { 289, 1 }, { 139, 3 } }) do
|
||||
assert(Bag.get(session.bag, pair[1]) == pair[2], 'preserved item ' .. pair[1])
|
||||
end
|
||||
assert(session.storage.items[1].qty == 999 and session.storage.items[2].qty == 119)
|
||||
assert(session.party[1].custom.sentinel == 'kept')
|
||||
print('PASS editor_native_bag_pc_continue')
|
||||
for _, pocket in ipairs({ 'ITEMS', 'KEY_ITEMS', 'POKE_BALLS', 'TM_CASE', 'BERRY_POUCH' }) do
|
||||
BagMenu.show(session.bag, { session = session, pocket = pocket })
|
||||
shot('u3_editor_preserved_' .. pocket:lower()); BagMenu.close(); U.wait(10)
|
||||
end
|
||||
Pc.show({ session = session, startMode = 'player_pc' })
|
||||
U.tap(game, 'a'); U.wait(20); U.tap(game, 'a'); U.wait(20)
|
||||
shot('u3_editor_pc_999_119'); Pc.close(); U.wait(10)
|
||||
for _, species in ipairs({ 25, 4 }) do
|
||||
if session.party[1].species ~= species then session.party[1], session.party[2] = session.party[2], session.party[1] end
|
||||
local before = session.party[1].exp
|
||||
assert(Bridge.startWild(Runtime._mod, game, { species = 129, level = 30 }, { fade = false }))
|
||||
local captured = false
|
||||
for frame = 1, 9000 do
|
||||
if not Battle.isActive() then break end
|
||||
if Battle._phase == 'command' and Ui._mode == 'menu' then
|
||||
if not captured then shot('u2_editor_battle_' .. species); captured = true end
|
||||
local state = Battle.getState()
|
||||
state.enemy.mon.hp = 1; state.enemy.mon.moves = { 150 }; state.enemy.mon.pp = { 40 }
|
||||
local present = Anim.present('enemy'); if present then present.displayHp = 1 end
|
||||
Ui._pendingCommand = { kind = 'move', move = 10, slot = 1, user = 'player' }
|
||||
Ui._mode = 'none'; U.wait(2)
|
||||
elseif Anim.vm() and Anim.vm():busy() then U.wait(1)
|
||||
elseif frame % 12 == 0 then U.tap(game, 'a')
|
||||
else U.wait(1) end
|
||||
end
|
||||
assert(not Battle.isActive(), 'battle timeout')
|
||||
U.wait(120)
|
||||
assert(session.party[1].exp > before and session.party[1].level >= 22, 'normal battle award for ' .. species)
|
||||
print('PASS editor_continue_battle_exp_' .. species)
|
||||
Summary.openMenu(session.party, 1, { session = session, page = 1 })
|
||||
shot('u2_editor_levelup_stats_' .. species); Summary.close(); U.wait(10)
|
||||
end
|
||||
assert(game:saveGame()); session = continue()
|
||||
assert(session.party[1].level >= 22 and session.party[2].level >= 22)
|
||||
assert(Bag.get(session.bag, 68) == 4 and session.storage.items[2].qty == 119)
|
||||
print('PASS editor_second_save_continue_preservation')
|
||||
print('PASS game3_editor_exp_roundtrip')
|
||||
end, debug.traceback)
|
||||
if not ok then print('FAIL game3_editor_exp_roundtrip ' .. tostring(err)) end
|
||||
love.event.quit(ok and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_hidden_item_persistence"
|
||||
return function(game)
|
||||
local failures = 0
|
||||
local function check(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
local function finish() love.event.quit(failures == 0 and 0 or 1) end
|
||||
for _ = 1, 900 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(240)
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Map = require("src.core.game3.map")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Field = require("src.core.game3.field")
|
||||
local Bag = require("src.core.game3.bag")
|
||||
local Message = require("src.ui.game3.message")
|
||||
local Start = require("src.ui.game3.start_menu")
|
||||
local BagMenu = require("src.ui.game3.bag_menu")
|
||||
local session = Runtime.getSession()
|
||||
if not check(session ~= nil, "hidden_new_game") then return finish() end
|
||||
session.bag = Bag.new()
|
||||
Flags.setVar(Space.store, nil, 0x4070, 1)
|
||||
Space.persistSession(nil, game)
|
||||
local function forest()
|
||||
Map.load(nil, game, "FR_VIRIDIAN_FOREST", { x = 28, y = 58, facing = "up" })
|
||||
Player.cellX, Player.cellY, Player.targetX, Player.targetY = 28, 58, 28, 58
|
||||
Player.px, Player.py, Player.facing = 448, 928, "up"
|
||||
session.x, session.y, session.facing = 28, 58, "up"
|
||||
U.wait(150)
|
||||
end
|
||||
forest()
|
||||
if not check(Field.hiddenItemAt(game, 28, 57, 0) ~= nil, "hidden_forest_antidote_present") then return finish() end
|
||||
U.tap(game, "a")
|
||||
U.wait(120)
|
||||
if not check(Message.isOpen() and Bag.get(session.bag, 14) == 1, "hidden_first_pickup") then return finish() end
|
||||
check(U.shot(game, DIR .. "/2364_antidote_found.png"), "hidden_pickup_screenshot_written")
|
||||
for _ = 1, 30 do
|
||||
if not Message.isOpen() then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(15)
|
||||
end
|
||||
U.tap(game, "start")
|
||||
U.wait(40)
|
||||
if not check(Start.isOpen(), "hidden_start_open") then return finish() end
|
||||
check(U.shot(game, DIR .. "/2364_start_after_pickup.png"), "hidden_start_screenshot_written")
|
||||
U.tap(game, "b")
|
||||
U.wait(30)
|
||||
U.tap(game, "a")
|
||||
U.wait(30)
|
||||
check(not Message.isOpen() and Bag.get(session.bag, 14) == 1, "hidden_no_duplicate_after_start")
|
||||
check(Flags.getFlag(Space.store, nil, 0x3E9) and Flags.getFlag(session, nil, 0x3E9), "hidden_live_and_saved_flags")
|
||||
check(not Field.useItemfinder(session, false), "hidden_itemfinder_ignores_collected")
|
||||
Map.load(nil, game, "FR_PALLET_TOWN", { x = 5, y = 7, facing = "down" })
|
||||
U.wait(60)
|
||||
forest()
|
||||
U.tap(game, "a")
|
||||
U.wait(30)
|
||||
check(not Message.isOpen() and Bag.get(session.bag, 14) == 1, "hidden_no_duplicate_after_map_return")
|
||||
U.tap(game, "start")
|
||||
U.wait(30)
|
||||
for _ = 1, #Start.ENTRIES do
|
||||
if Start.ENTRIES[Start.cursor].id == "bag" then break end
|
||||
U.tap(game, "down")
|
||||
U.wait(5)
|
||||
end
|
||||
U.tap(game, "a")
|
||||
U.wait(60)
|
||||
if check(BagMenu.isOpen() and BagMenu.currentPocket() == "ITEMS", "hidden_bag_quantity_visible") then
|
||||
check(U.shot(game, DIR .. "/2364_one_antidote_after_menu_and_map.png"), "hidden_quantity_screenshot_written")
|
||||
end
|
||||
finish()
|
||||
end
|
||||
@@ -0,0 +1,125 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_marowak_2380"
|
||||
return function(game)
|
||||
local failures = 0
|
||||
local function check(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
local function finish() love.event.quit(failures == 0 and 0 or 1) end
|
||||
for _ = 1, 900 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(240)
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Map = require("src.core.game3.map")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Party = require("src.core.game3.party")
|
||||
local Bag = require("src.core.game3.bag")
|
||||
local Battle = require("src.core.game3.battle")
|
||||
local Collision = require("src.core.game3.collision")
|
||||
local Message = require("src.ui.game3.message")
|
||||
local session = Runtime.getSession()
|
||||
if not check(session ~= nil, "marowak_new_game") then return finish() end
|
||||
session.party = {}
|
||||
Party.giveMon(session, 150, 100)
|
||||
session.party[1].moves, session.party[1].pp = { 94 }, { 10 }
|
||||
session.party[1].maxPp = { 10 }
|
||||
Bag.add(session.bag, 359, 1)
|
||||
local function place(x, y, facing)
|
||||
Player.moving, Player.progress = false, 0
|
||||
Player.cellX, Player.cellY, Player.targetX, Player.targetY = x, y, x, y
|
||||
Player.px, Player.py, Player.facing = x * 16, y * 16, facing
|
||||
session.x, session.y, session.facing = x, y, facing
|
||||
end
|
||||
local function walk(dir)
|
||||
for _ = 1, 30 do
|
||||
U.hold(game, dir, 1)
|
||||
if Player.moving then break end
|
||||
end
|
||||
for _ = 1, 120 do
|
||||
if not Player.moving then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(4)
|
||||
end
|
||||
Map.load(nil, game, "FR_POKEMON_TOWER_6F", { x = 11, y = 14, facing = "down" })
|
||||
place(11, 14, "down")
|
||||
U.wait(150)
|
||||
check(Flags.getVar(Space.store, nil, 0x4059) == 0, "marowak_scene_starts_zero")
|
||||
walk("down")
|
||||
for _ = 1, 300 do
|
||||
if Battle.isActive() then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(8)
|
||||
end
|
||||
if not check(Battle.isActive(), "marowak_coord_script_started_battle") then return finish() end
|
||||
local farewell = false
|
||||
for _ = 1, 1800 do
|
||||
if not Battle.isActive() and Message.isOpen()
|
||||
and Message.currentPage():lower():find("mother", 1, true) then
|
||||
farewell = true
|
||||
break
|
||||
end
|
||||
U.tap(game, "a")
|
||||
U.wait(8)
|
||||
end
|
||||
if not check(farewell and session.battleOutcome == 1, "marowak_win_reaches_farewell") then return finish() end
|
||||
U.wait(150)
|
||||
check(U.shot(game, DIR .. "/2380_marowak_mothers_spirit_farewell.png"), "marowak_farewell_screenshot_written")
|
||||
for _ = 1, 300 do
|
||||
if not Message.isOpen() and not Space.vm:isRunning() then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(8)
|
||||
end
|
||||
if not check(Flags.getVar(Space.store, nil, 0x4059) == 1, "marowak_scene_completed") then return finish() end
|
||||
walk("up")
|
||||
walk("down")
|
||||
check(Player.cellX == 11 and Player.cellY == 15 and not Space.vm:isRunning()
|
||||
and not Battle.isActive(), "marowak_first_trigger_stays_cleared")
|
||||
place(13, 16, "left")
|
||||
walk("left")
|
||||
check(Player.cellX == 12 and Player.cellY == 16 and not Space.vm:isRunning()
|
||||
and not Battle.isActive(), "marowak_second_trigger_stays_cleared")
|
||||
walk("left")
|
||||
if not check(Player.cellX == 11 and Player.cellY == 16
|
||||
and Collision.isStairWarp(game, 11, 16, "left") ~= nil,
|
||||
"marowak_standing_on_7f_stairs") then return finish() end
|
||||
walk("left")
|
||||
U.wait(180)
|
||||
if not check(Runtime.getSession().map == "FR_POKEMON_TOWER_7F", "marowak_reached_7f") then return finish() end
|
||||
check(U.shot(game, DIR .. "/2380_tower_7f_after_marowak.png"), "marowak_7f_screenshot_written")
|
||||
if not check(game:saveGame() == true, "marowak_save_written") then return finish() end
|
||||
game:_handleBootAction({ action = "continue" })
|
||||
U.wait(60)
|
||||
for _ = 1, 400 do
|
||||
if game.phase ~= "quest_log" then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(6)
|
||||
end
|
||||
U.wait(180)
|
||||
session = Runtime.getSession()
|
||||
check(session and session.map == "FR_POKEMON_TOWER_7F"
|
||||
and Flags.getVar(Space.store, nil, 0x4059) == 1, "marowak_continue_keeps_scene_and_7f")
|
||||
Map.load(nil, game, "FR_POKEMON_TOWER_6F", { x = 11, y = 14, facing = "down" })
|
||||
place(11, 14, "down")
|
||||
U.wait(90)
|
||||
walk("down")
|
||||
check(not Space.vm:isRunning() and not Battle.isActive()
|
||||
and Flags.getVar(Space.store, nil, 0x4059) == 1, "marowak_no_respawn_after_reload")
|
||||
walk("down")
|
||||
if not check(Player.cellX == 11 and Player.cellY == 16
|
||||
and Collision.isStairWarp(game, 11, 16, "left") ~= nil,
|
||||
"marowak_standing_on_7f_stairs_after_reload") then return finish() end
|
||||
walk("left")
|
||||
U.wait(180)
|
||||
if check(session.map == "FR_POKEMON_TOWER_7F", "marowak_7f_access_after_reload") then
|
||||
check(U.shot(game, DIR .. "/2380_tower_7f_after_save_reload.png"), "marowak_reload_screenshot_written")
|
||||
end
|
||||
finish()
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local Doors = require("src.core.game3.doors")
|
||||
local draws, rectangles = {}, {}
|
||||
love = { graphics = {
|
||||
draw = function(...) draws[#draws + 1] = {...} end,
|
||||
rectangle = function(...) rectangles[#rectangles + 1] = {...} end,
|
||||
setColor = function() end,
|
||||
line = function() end,
|
||||
} }
|
||||
|
||||
local image, quad = {}, {}
|
||||
local cases = {
|
||||
{ "native_defaults", 120, 56, nil, nil, 16, true },
|
||||
{ "portrait_visible", 187, 398, 390, 844, 16, true },
|
||||
{ "retina_portrait_visible", 139, 293, 294, 634, 16, true },
|
||||
{ "landscape_visible", 400, 120, 844, 390, 16, true },
|
||||
{ "survey_visible", 800, 420, 1688, 780, 32, true },
|
||||
{ "native_right_edge", 239, 60, nil, nil, 16, true },
|
||||
{ "native_right_offscreen", 240, 60, nil, nil, 16, false },
|
||||
{ "native_bottom_offscreen", 100, 160, nil, nil, 16, false },
|
||||
{ "left_partial", -15, 60, 390, 844, 16, true },
|
||||
{ "left_offscreen", -16, 60, 390, 844, 16, false },
|
||||
{ "right_offscreen", 390, 60, 390, 844, 16, false },
|
||||
{ "tall_top_partial", 100, -15, 390, 844, 32, true },
|
||||
{ "tall_top_offscreen", 100, -16, 390, 844, 32, false },
|
||||
{ "tall_bottom_partial", 100, 859, 390, 844, 32, true },
|
||||
{ "tall_bottom_offscreen", 100, 860, 390, 844, 32, false },
|
||||
}
|
||||
|
||||
local failures = 0
|
||||
for _, t in ipairs(cases) do
|
||||
draws, rectangles = {}, {}
|
||||
Doors._sheets.Test = {
|
||||
image = image, quads = { [1] = quad }, frames = 3,
|
||||
frame_width = 16, frame_height = t[6],
|
||||
}
|
||||
Doors._activeAnim = { x = 73, y = 91, tile = "Test", frame = 1 }
|
||||
Doors.draw(73 * 16 - t[2], 91 * 16 - t[3], t[4], t[5])
|
||||
local ok = #draws == (t[7] and 1 or 0) and #rectangles == (t[7] and 1 or 0)
|
||||
if t[7] and draws[1] then
|
||||
local d = draws[1]
|
||||
ok = ok and d[1] == image and d[2] == quad and d[3] == t[2]
|
||||
and d[4] == t[3] - (t[6] > 16 and 16 or 0)
|
||||
end
|
||||
print((ok and "PASS " or "FAIL ") .. t[1])
|
||||
if not ok then failures = failures + 1 end
|
||||
end
|
||||
|
||||
draws, rectangles = {}, {}
|
||||
Doors._sheets.Test = false
|
||||
Doors._activeAnim = { x = 25, y = 30, tile = "Test", frame = 1 }
|
||||
Doors.draw(0, 0, 844, 780)
|
||||
assert(#draws == 0 and #rectangles > 0, "fallback on enlarged viewport")
|
||||
print("PASS fallback_visible")
|
||||
assert(failures == 0, tostring(failures) .. " viewport cases failed")
|
||||
print("PASS doors_viewport")
|
||||
@@ -182,13 +182,30 @@ eq(Pokemon.speciesOf(boxed), ARON, "and it is still ARON")
|
||||
local again = Schema.fromSaveTable(Schema.toSaveTable(loaded))
|
||||
eq(again.party[1].speciesNumbering, Pokemon.NUMBERING_INTERNAL, "the tag survives a save round trip")
|
||||
|
||||
local hostExp = require("src.core.game3.summary_data").expForLevel(Pokemon.growthRate(WURMPLE), 8) - 1
|
||||
local hostSave = {
|
||||
schemaVersion = 1,
|
||||
party = { { species = "WURMPLE", level = 7, hp = 10, maxHp = 10 } },
|
||||
party = { { species = "WURMPLE", level = 7, hp = 10, maxHp = 10, exp = hostExp, personality = 0 } },
|
||||
}
|
||||
local hostLoaded = Schema.fromSaveTable(hostSave)
|
||||
eq(hostLoaded.party[1].speciesNumbering, nil, "a host mon keyed by name is left untagged")
|
||||
eq(Pokemon.speciesOf(hostLoaded.party[1]), WURMPLE, "and still resolves by name")
|
||||
eq(hostLoaded.party[1].speciesNumbering, Pokemon.NUMBERING_INTERNAL, "a named host mon is normalized to internal numbering")
|
||||
eq(hostLoaded.party[1].species, WURMPLE, "the persisted species is numeric internal WURMPLE")
|
||||
eq(hostLoaded.party[1].speciesId, WURMPLE, "the species alias agrees with WURMPLE")
|
||||
eq(hostLoaded.party[1].exp, hostExp, "normalization preserves earned EXP")
|
||||
eq(hostLoaded.party[1].hp, 10, "normalization preserves damaged HP")
|
||||
eq(Evolution.levelTarget(hostLoaded.party[1], national), SILCOON, "normalization preserves the WURMPLE evolution branch")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local hostRoundtrip = Schema.fromSaveTable(SaveData.decode(SaveData.encode(Schema.toSaveTable(hostLoaded))))
|
||||
eq(Pokemon.speciesOf(hostRoundtrip.party[1]), WURMPLE, "serialized roundtrip preserves WURMPLE rather than national NINCADA")
|
||||
eq(hostRoundtrip.party[1].exp, hostExp, "serialized roundtrip preserves earned EXP")
|
||||
local rewards = require("src.core.game3.battle.experience").awardFoe({
|
||||
playerParty = hostRoundtrip.party, player = { mon = hostRoundtrip.party[1], partyIndex = 1 },
|
||||
wild = true, session = hostRoundtrip,
|
||||
}, { species = 113, level = 10, mon = { species = 113, level = 10 } },
|
||||
{ partyIndices = { 1 }, getOpts = function() return {} end })
|
||||
eq(#rewards, 1, "normalized named mon remains an actual battle EXP recipient")
|
||||
check(hostRoundtrip.party[1].exp > hostExp and hostRoundtrip.party[1].level >= 8,
|
||||
"battle EXP crosses the normalized WURMPLE growth threshold")
|
||||
|
||||
print("[test] 7. a mon handed over by a script is stamped when it is built")
|
||||
local Party = require("src.core.game3.party")
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
require("src.core.GameVersion").set("firered")
|
||||
local Field = require("src.core.game3.field")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Bag = require("src.core.game3.bag")
|
||||
local Items = require("src.core.game3.items")
|
||||
local Message = require("src.ui.game3.message")
|
||||
local Hud = require("src.ui.game3.hud")
|
||||
local Start = require("src.ui.game3.start_menu")
|
||||
local Schema = require("src.core.game3.save_schema_firered")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local hidden = { type = "hidden_item", x = 28, y = 57, flag = 0x3E9, item = 14, quantity = 1 }
|
||||
local s = Schema.newGame({ name = "RED" })
|
||||
s.map, s.x, s.y, s.playerX, s.playerY = "FR_VIRIDIAN_FOREST", 28, 58, 28, 58
|
||||
local game = { session = s, data = { maps = { [s.map] = { bgEvents = { hidden } } } } }
|
||||
Field._game, Field._session, Runtime._game, Runtime.session = game, s, game, s
|
||||
Space.store = Flags.newStore()
|
||||
Flags.setVar(Space.store, nil, 0x4070, 1)
|
||||
Space.persistSession(nil, game)
|
||||
s.bag = Bag.new()
|
||||
assert(Field.pickUpHiddenItem(game, hidden))
|
||||
Message.close()
|
||||
assert(Flags.getFlag(Space.store, nil, hidden.flag), "pickup must mark the live store")
|
||||
assert(Flags.getFlag(s, nil, hidden.flag), "pickup must persist the session snapshot")
|
||||
for _ = 1, 3 do
|
||||
Hud.openStartMenu(game, s)
|
||||
assert(Start.isOpen())
|
||||
Start.close(true)
|
||||
assert(not Field.hiddenItemAt(game, 28, 57, 0))
|
||||
assert(not Field.pickUpHiddenItem(game, hidden))
|
||||
assert(Bag.get(s.bag, 14) == 1)
|
||||
assert(not Field.useItemfinder(s, false))
|
||||
end
|
||||
print("PASS hidden_pickup_survives_start_menu")
|
||||
s.flags = {}
|
||||
assert(not Field.hiddenItemAt(game, 28, 57, 0))
|
||||
assert(not Field.useItemfinder(s, false), "Itemfinder must prefer live flags over stale snapshots")
|
||||
Space.persistSession(nil, game)
|
||||
local restored = Schema.fromSaveTable(SaveData.decode(SaveData.encode(Schema.toSaveTable(s))))
|
||||
assert(Flags.getFlag(restored, nil, hidden.flag))
|
||||
assert(Bag.get(restored.bag, 14) == 1)
|
||||
print("PASS hidden_itemfinder_and_save_roundtrip")
|
||||
Flags.setFlag(Space.store, nil, hidden.flag, false)
|
||||
Space.persistSession(nil, game)
|
||||
s.bag = Bag.new()
|
||||
assert(Bag.add(s.bag, 14, Items.GAME3_MAX_QTY))
|
||||
assert(Field.pickUpHiddenItem(game, hidden))
|
||||
Message.close()
|
||||
assert(not Flags.getFlag(Space.store, nil, hidden.flag))
|
||||
assert(not Flags.getFlag(s, nil, hidden.flag))
|
||||
assert(Field.hiddenItemAt(game, 28, 57, 0) == hidden)
|
||||
assert(Field.useItemfinder(s, false))
|
||||
assert(Bag.remove(s.bag, 14, Items.GAME3_MAX_QTY))
|
||||
assert(Field.pickUpHiddenItem(game, hidden))
|
||||
Message.close()
|
||||
assert(Bag.get(s.bag, 14) == 1)
|
||||
assert(Flags.getFlag(Space.store, nil, hidden.flag))
|
||||
print("PASS hidden_full_bag_does_not_consume")
|
||||
local isolated = { map = s.map, flags = {}, bag = Bag.new(), x = 28, y = 58 }
|
||||
Field._session = isolated
|
||||
assert(Field.hiddenItemAt(game, 28, 57, 0) == hidden)
|
||||
assert(Field.useItemfinder(isolated, false))
|
||||
assert(Field.pickUpHiddenItem(game, hidden))
|
||||
Message.close()
|
||||
assert(Flags.getFlag(isolated, nil, hidden.flag))
|
||||
assert(Bag.get(isolated.bag, 14) == 1)
|
||||
assert(Bag.get(s.bag, 14) == 1)
|
||||
print("PASS hidden_isolated_session_fallback")
|
||||
@@ -60,8 +60,7 @@ mock_bytes[15] = 20; mock_bytes[16] = 0 -- y = 20
|
||||
mock_bytes[17] = 0 -- elevation = 0
|
||||
mock_bytes[18] = 7 -- kind = 7 (BG_EVENT_HIDDEN_ITEM)
|
||||
mock_bytes[21] = 14; mock_bytes[22] = 0 -- item = 14 (ANTIDOTE)
|
||||
-- info: hiddenItemId = 1, quantity = 1 (1 << 9 = 0x0200) -> 0x0201
|
||||
mock_bytes[23] = 0x01; mock_bytes[24] = 0x02
|
||||
mock_bytes[23] = 0x01; mock_bytes[24] = 0x01
|
||||
|
||||
-- Event 2: Hidden Potion at offset 24 (underfoot bit 15 set)
|
||||
mock_bytes[25] = 3; mock_bytes[26] = 0 -- x = 3
|
||||
@@ -69,8 +68,7 @@ mock_bytes[27] = 4; mock_bytes[28] = 0 -- y = 4
|
||||
mock_bytes[29] = 0 -- elevation = 0
|
||||
mock_bytes[30] = 7 -- kind = 7 (BG_EVENT_HIDDEN_ITEM)
|
||||
mock_bytes[33] = 13; mock_bytes[34] = 0 -- item = 13 (POTION)
|
||||
-- info: hiddenItemId = 0, quantity = 1 (1 << 9 = 0x0200), underfoot = 1 (1 << 15 = 0x8000) -> 0x8200
|
||||
mock_bytes[35] = 0x00; mock_bytes[36] = 0x82
|
||||
mock_bytes[35] = 0x00; mock_bytes[36] = 0x81
|
||||
|
||||
local rom = create_mock_rom(mock_bytes)
|
||||
local mapEventsOff = 0x08000000
|
||||
@@ -130,6 +128,22 @@ assert(potion.flag == 0x3E8 + 0, "flag is 0x3E8 (1000)")
|
||||
|
||||
print("[OK] BgEvent parsing correctly extracted hidden items and unpacked union fields.")
|
||||
|
||||
for _, hiddenId in ipairs({ 0, 1, 190, 255 }) do
|
||||
for _, quantity in ipairs({ 1, 2, 3, 63, 64, 127 }) do
|
||||
for _, underfoot in ipairs({ false, true }) do
|
||||
full_bytes[32 + 23] = hiddenId
|
||||
full_bytes[32 + 24] = quantity + (underfoot and 128 or 0)
|
||||
local event = ExtractMapEvents.parseMapEvents(romFull, 0x08000000).bgEvents[2]
|
||||
assert(event.hiddenItemId == hiddenId, "quantity must not leak into hidden item flag")
|
||||
assert(event.flag == 1000 + hiddenId and event.flag < 1256, "hidden flags stay in their eight-bit range")
|
||||
assert(event.quantity == quantity, "all seven quantity bits must survive extraction")
|
||||
assert(event.underfoot == underfoot, "underfoot must not change quantity or flag")
|
||||
end
|
||||
end
|
||||
end
|
||||
full_bytes[32 + 23], full_bytes[32 + 24] = 1, 1
|
||||
print("PASS hidden_item_flag_quantity_bit_boundaries")
|
||||
|
||||
print("=== [Test 2: Hidden Item Field Pickup & Bag Integration] ===")
|
||||
|
||||
local game = {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
require("src.core.GameVersion").set("firered")
|
||||
local Ctx = require("src.core.game3.scripting.ctx")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Std = require("src.core.game3.scripting.stdscripts")
|
||||
local Natives = require("src.core.game3.scripting.natives")
|
||||
local Enc = require("src.core.game3.encounters")
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Bag = require("src.core.game3.bag")
|
||||
local session = { bag = Bag.new(), party = {} }
|
||||
Bag.add(session.bag, 359, 1)
|
||||
Runtime.session = session
|
||||
for _, case in ipairs({ { "win", 1, 0 }, { "ran", 4, 1 }, { "lose", 2, 1 },
|
||||
{ 1, 1, 0 }, { 4, 4, 1 }, { 7, 7, 1 } }) do
|
||||
local ctx, callback = Ctx.new(), nil
|
||||
Enc._pendingWild = { species = 105, level = 30 }
|
||||
Natives.special(ctx, Std.SPECIAL.StartMarowakBattle, {
|
||||
startWildBattle = function(foe, done)
|
||||
assert(foe.ghost and foe.ghostUnveiled)
|
||||
callback = done
|
||||
end,
|
||||
})
|
||||
assert(callback and not ctx.nativePoll())
|
||||
callback(case[1])
|
||||
assert(ctx.nativePoll())
|
||||
assert(Flags.getVar(nil, ctx, 0x800D) == case[3], "Marowak boolean result for " .. tostring(case[1]))
|
||||
assert(ctx.lastBattleOutcome == case[2])
|
||||
Natives.special(ctx, Std.SPECIAL.GetBattleOutcome, {})
|
||||
assert(Flags.getVar(nil, ctx, 0x800D) == case[2])
|
||||
Enc._pendingWild = { species = 249, level = 70 }
|
||||
Natives.special(ctx, Std.SPECIAL.StartLegendaryBattle, {
|
||||
startWildBattle = function(_, done) callback = done end,
|
||||
})
|
||||
callback(case[1])
|
||||
assert(ctx.nativePoll())
|
||||
assert(Flags.getVar(nil, ctx, 0x800D) == case[2])
|
||||
end
|
||||
print("PASS marowak_boolean_and_raw_outcomes")
|
||||
local Cache = require("tests.game3_cache")
|
||||
local bundle = assert(Cache.bundle("scripts/events.lua", { native = true }), Cache.reason)
|
||||
local Vm = require("src.core.game3.scripting.vm")
|
||||
local Adapters = require("src.core.game3.scripting.adapters")
|
||||
local events = assert(bundle.events.FR_POKEMON_TOWER_6F.coordEvents)
|
||||
local key = assert(events[1].scriptKey)
|
||||
for _, case in ipairs({ { "win", 1 }, { "ran", 0 } }) do
|
||||
local store, messages, callback = Flags.newStore(), {}, nil
|
||||
local adapters = Adapters.stub({ onMessage = function(text) messages[#messages + 1] = text end,
|
||||
lookupMovement = function(id) return bundle.movements[id] end })
|
||||
adapters.startWildBattle = function(foe, done)
|
||||
assert(foe.species == 105 and foe.level == 30)
|
||||
callback = done
|
||||
end
|
||||
local vm = Vm.new({ store = store, scripts = bundle.scripts, text = bundle.text,
|
||||
movements = bundle.movements, adapters = adapters })
|
||||
assert(vm:start(key))
|
||||
for _ = 1, 100 do
|
||||
if callback then break end
|
||||
vm:tick()
|
||||
end
|
||||
assert(callback, "imported script must reach Marowak native")
|
||||
assert(Flags.getVar(store, nil, 0x4059) == 0)
|
||||
callback(case[1])
|
||||
for _ = 1, 2000 do
|
||||
if not vm:isRunning() then break end
|
||||
vm:tick()
|
||||
end
|
||||
assert(not vm:isRunning(), "imported Tower script must finish")
|
||||
assert(Flags.getVar(store, nil, 0x4059) == case[2], "imported Tower scene for " .. case[1])
|
||||
local farewell = table.concat(messages, "\n"):lower():find("mother", 1, true) ~= nil
|
||||
assert(farewell == (case[1] == "win"), "farewell follows the winning branch only")
|
||||
for _, event in ipairs(events) do
|
||||
assert(event.var == 0x4059 and event.value == 0)
|
||||
assert((Flags.getVar(store, nil, event.var) ~= event.value) == (case[1] == "win"))
|
||||
end
|
||||
end
|
||||
print("PASS marowak_imported_script_progression")
|
||||
@@ -0,0 +1,341 @@
|
||||
package.path = './?.lua;./?/init.lua;./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua;' .. package.path
|
||||
_G.love = require('tests.love_stub')
|
||||
assert(require('tests.game3_cache').mount('pokemon/meta.lua'))
|
||||
local P = require('src.core.game3.pokemon')
|
||||
P.install(nil)
|
||||
local E = require('src.core.game3.battle.experience')
|
||||
local Schema = require('src.core.game3.save_schema_firered')
|
||||
local Bag = require('src.core.game3.bag')
|
||||
local Storage = require('src.core.game3.storage')
|
||||
local SD = require('src.core.SaveData')
|
||||
local IO = require('SaveIO')
|
||||
local App = require('tools.save-editor.App')
|
||||
local Ops = require('Ops')
|
||||
local Gen = require('Gen')
|
||||
local Copy = require('src.mods.Merge').deepCopy
|
||||
local function eq(a, b, label)
|
||||
if type(a) == 'table' and type(b) == 'table' then
|
||||
for k, v in pairs(a) do eq(v, b[k], label .. '.' .. tostring(k)) end
|
||||
for k in pairs(b) do assert(a[k] ~= nil, label .. ': extra ' .. tostring(k)) end
|
||||
else assert(a == b, label .. ': ' .. tostring(a) .. ' ~= ' .. tostring(b)) end
|
||||
end
|
||||
local function mon(id)
|
||||
local m = { species = id, speciesId = id, level = 20, personality = 123,
|
||||
ivs = { hp = 20, atk = 14, def = 12, spe = 18, spa = 24, spd = 6 },
|
||||
evs = { hp = 10, atk = 8, def = 6, spe = 4, spa = 2, spd = 0 },
|
||||
moves = { 33, 45 }, pp = { 30, 35 }, maxPp = { 35, 40 }, ppBonusesPacked = 5,
|
||||
heldItem = 13, otId = 123, otSecretId = 321, otName = 'OTHER', nickname = 'KEPT',
|
||||
metLocation = 88, metLevel = 5, pokeball = 2, friendship = 133, pokerus = 17,
|
||||
custom = { sentinel = 'kept' } }
|
||||
E.syncExpToLevel(m); P.applyStats(m); m.hp = m.maxHp - 3
|
||||
return m
|
||||
end
|
||||
local files = {}
|
||||
local function load(native)
|
||||
local path = os.tmpname()
|
||||
files[#files + 1] = path
|
||||
local f = assert(io.open(path, 'wb')); f:write(SD.encode(native)); f:close()
|
||||
App.load(path, { version = 'firered', embedded = true })
|
||||
return App.getState(), path
|
||||
end
|
||||
local function saved(path)
|
||||
assert(App.save())
|
||||
return assert(IO.load(path))
|
||||
end
|
||||
local function award(session)
|
||||
return E.awardFoe({ playerParty = session.party, player = { mon = session.party[1], partyIndex = 1 },
|
||||
wild = true, session = session }, { species = 113, level = 40, mon = { species = 113, level = 40 } },
|
||||
{ partyIndices = { 1 }, getOpts = function() return {} end })
|
||||
end
|
||||
local function native(id)
|
||||
local session = Schema.newGame({ name = 'RED' })
|
||||
session.party = { mon(id or 25), mon(6) }
|
||||
session.storage.boxes[1].mons[7] = mon(9)
|
||||
session.storage.boxes[1].name, session.storage.boxes[1].wallpaper = 'KEEP', 8
|
||||
session.storage.currentBox = 3
|
||||
for _, pair in ipairs({ { 13, 5 }, { 68, 2 }, { 4, 10 }, { 360, 1 }, { 289, 1 }, { 139, 3 } }) do
|
||||
assert(Bag.add(session.bag, pair[1], pair[2]))
|
||||
end
|
||||
session.storage.items = { { id = 13, qty = 999 }, { id = 68, qty = 120 }, { id = 4, qty = 10 } }
|
||||
return SD.decode(SD.encode(Schema.toSaveTable(session)))
|
||||
end
|
||||
|
||||
local metadata = { 'personality', 'ivs', 'evs', 'moves', 'pp', 'maxPp', 'ppBonusesPacked', 'heldItem',
|
||||
'otId', 'otSecretId', 'otName', 'nickname', 'metLocation', 'metLevel', 'pokeball', 'friendship', 'pokerus', 'custom' }
|
||||
if os.getenv('POKEPORT_SAVE_EDITOR_ITEMS_ONLY') ~= '1' then
|
||||
local species = { 25, 4 }
|
||||
local groups = {}
|
||||
for id = 1, 412 do
|
||||
if P.isInternalSpecies(id) then
|
||||
local gr = P.growthRate(id)
|
||||
if gr ~= nil and not groups[gr] then groups[gr] = id; species[#species + 1] = id end
|
||||
end
|
||||
end
|
||||
local groupCount = 0
|
||||
for _ in pairs(groups) do groupCount = groupCount + 1 end
|
||||
assert(groupCount == 6, 'real pack includes all six growth groups')
|
||||
for _, id in ipairs(species) do
|
||||
for _, level in ipairs({ 0, 15, 21 }) do
|
||||
local source = native(id)
|
||||
local before = Copy(source.party[1])
|
||||
local S, path = load(source)
|
||||
eq(S.save.party[1].maxHp, before.maxHp, 'hydrate stats')
|
||||
eq(S.save.party[1].hp, before.hp, 'hydrate preserves current HP')
|
||||
if level > 0 then assert(Ops.setLevel(S, S.save.party[1], level)) end
|
||||
S.save.party[1].exp = require('src.core.game3.summary_data').expForLevel(P.growthRate(id), S.save.party[1].level + 1) - 1
|
||||
local output = saved(path)
|
||||
eq(output.party[1].species, id, 'numeric species persistence')
|
||||
for _, key in ipairs(metadata) do eq(output.party[1][key], before[key], 'mon metadata ' .. key) end
|
||||
local reloaded = Schema.fromSaveTable(output)
|
||||
local m = reloaded.party[1]
|
||||
local stats = P.calcStats(id, m.level, m.ivs, m.evs, m.personality)
|
||||
eq(m.maxHp, stats.maxHp, 'runtime stats')
|
||||
local oldExp, oldLevel = m.exp, m.level
|
||||
local rewards = award(reloaded)
|
||||
eq(#rewards, 1, 'actual award count')
|
||||
assert(m.exp > oldExp and m.level > oldLevel, 'actual battle EXP and level crossing')
|
||||
App.unload()
|
||||
end
|
||||
end
|
||||
print('PASS native_editor_award_all_growth_groups')
|
||||
|
||||
do
|
||||
local source = native()
|
||||
source.party[1].species, source.party[1].maxHp, source.party[1].hp = 'PIKACHU', 999, 7
|
||||
source.storage.boxes[1].mons[7].species = 'BLASTOISE'
|
||||
local loaded = Schema.fromSaveTable(source)
|
||||
eq(loaded.party[1].species, 25, 'legacy named party repair')
|
||||
eq(loaded.party[1].hp, 7, 'legacy repair no healing')
|
||||
eq(loaded.storage.boxes[1].mons[7].species, 9, 'legacy named box repair')
|
||||
assert(#award(loaded) == 1, 'legacy repaired mon receives award')
|
||||
print('PASS legacy_named_species_repair')
|
||||
end
|
||||
|
||||
do
|
||||
local S, path = load(native())
|
||||
local m = S.save.party[1]
|
||||
assert(Ops.setMove(S, m, 1, 57))
|
||||
eq(P.moveIdAt(m, 1), 57, 'numeric move replacement')
|
||||
eq(m.pp[1], P.movePp(57), 'new move PP')
|
||||
eq(m.ppBonusesPacked, 4, 'reset replaced slot bonus only')
|
||||
eq(m.moves[2], 45, 'unaffected move slot')
|
||||
eq(m.pp[2], 35, 'unaffected PP slot')
|
||||
assert(Ops.clearMove(S, m, 1)); eq(m.pp[1], nil, 'clear parallel PP')
|
||||
assert(Ops.setMove(S, m, 1, 57))
|
||||
eq(P.moveIdAt(Schema.fromSaveTable(saved(path)).party[1], 1), 57, 'saved native move')
|
||||
assert(S.save.boxes[1][7], 'sparse slot exposed')
|
||||
S.selectedParty, S.selectedBox, S.selectedBoxSlot = 2, 1, 2
|
||||
assert(Ops.deposit(S))
|
||||
eq(S.save.storage.boxes[1].mons[2].species, 6, 'deposit exact empty slot')
|
||||
eq(S.save.storage.boxes[1].mons[7].species, 9, 'untouched sparse slot')
|
||||
S.selectedBoxSlot = 7; assert(Ops.withdraw(S))
|
||||
eq(S.save.storage.boxes[1].mons[7], nil, 'withdraw exact sparse slot')
|
||||
local output = saved(path)
|
||||
eq(output.storage.boxes[1].name, 'KEEP', 'box name')
|
||||
eq(output.storage.boxes[1].wallpaper, 8, 'box wallpaper')
|
||||
eq(output.storage.currentBox, 3, 'current box preserved')
|
||||
eq(Schema.fromSaveTable(output).storage.boxes[1].mons[2].species, 6, 'runtime sparse deposit')
|
||||
App.unload()
|
||||
print('PASS native_moves_sparse_box_roundtrip')
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local source = native()
|
||||
source.extra = { keep = true }
|
||||
source.bag.pockets.ITEMS[1].custom = 'slot metadata'
|
||||
source.storage.items[2].custom = 'PC metadata'
|
||||
source.storage.custom = 'storage metadata'
|
||||
source.bag.pockets.ITEMS[#source.bag.pockets.ITEMS + 1] = { id = 2001, qty = 17, custom = 'foreign' }
|
||||
source.storage.items[#source.storage.items + 1] = { id = 2002, qty = 77, custom = 'foreign PC' }
|
||||
local S, path = load(source)
|
||||
local repeated = Copy(S.save)
|
||||
Gen.hydrateSave(S.data, S.save); eq(S.save, repeated, 'idempotent hydration')
|
||||
local boxRef = S.save.storage.boxes[1].mons[7]
|
||||
assert(Ops.addToBag(S, 'RARE_CANDY')); assert(Ops.bagAdjust(S, '68', 1))
|
||||
eq(S.save.inventory.pockets, nil, 'clean flat projection')
|
||||
assert(Ops.pcAdjust(S, 'RARE_CANDY', -1)); assert(Ops.addToPc(S, 68))
|
||||
eq(S.save.storage.boxes[1].mons[7], boxRef, 'item edit keeps selected mon identity')
|
||||
eq(S.save.pcItems['68'], 120, 'PC 120 decrement and add')
|
||||
eq(S.save.inventory['68'], 4, 'same ID aliases combined')
|
||||
local output = saved(path)
|
||||
local expected = Copy(source.bag.pockets); expected.ITEMS[2].qty = 4
|
||||
eq(output.bag.pockets, expected, 'full untouched pocket and metadata preservation')
|
||||
eq(output.storage.items, source.storage.items, 'full PC order quantities metadata')
|
||||
eq(output.extra, source.extra, 'unknown save fields')
|
||||
eq(output.storage.custom, source.storage.custom, 'storage metadata')
|
||||
local loaded = Schema.fromSaveTable(Copy(output))
|
||||
eq(Bag.get(loaded.bag, 68), 4, 'runtime candy')
|
||||
eq(loaded.storage.items[2].qty, 120, 'runtime PC above 99')
|
||||
App.unload(); App.load(path, { version = 'firered', embedded = true }); S = App.getState()
|
||||
assert(Ops.bagMax(S, 'RARE_CANDY')); assert(Ops.pcMax(S, '68'))
|
||||
local before = Copy(S.save); S.dirty = false
|
||||
assert(not Ops.addToBag(S, 68)); eq(S.save, before, 'bag 999 rejection full rollback'); eq(S.dirty, false, 'failed bag not dirty')
|
||||
assert(not Ops.addToPc(S, 'RARE_CANDY')); eq(S.save, before, 'PC 999 rejection full rollback')
|
||||
assert(Ops.bagAdjust(S, 68, -1)); assert(Ops.pcAdjust(S, 68, -1))
|
||||
eq(S.save.inventory['68'], 998, 'bag 999 decrement'); eq(S.save.pcItems['68'], 998, 'PC 999 decrement')
|
||||
assert(Ops.bagMaxAll(S)); assert(Ops.pcMaxAll(S))
|
||||
for _, slot in ipairs(S.save.bag.pockets.KEY_ITEMS) do eq(slot.qty, 1, 'containers not inflated') end
|
||||
assert(Ops.bagDrop(S, 68)); assert(Ops.pcDrop(S, 68))
|
||||
local final = Schema.fromSaveTable(saved(path))
|
||||
eq(Bag.get(final.bag, 68), 0, 'drop persisted')
|
||||
final.storage.items[1].qty = 998
|
||||
assert(Storage.depositItem(final, 'ITEMS', 1, 1)); assert(Storage.withdrawItem(final, 1, 1))
|
||||
App.unload()
|
||||
print('PASS native_bag_pc_atomic_preservation')
|
||||
end
|
||||
|
||||
do
|
||||
local source = native()
|
||||
source.bag = Bag.new(); source.storage.items = {}
|
||||
source.inventory = { stacks = { POTION = 999 } }; source.pcItems = { POTION = 999 }
|
||||
local S, path = load(source)
|
||||
eq(S.save.inventory, {}, 'native empty bag authoritative'); eq(S.save.pcItems, {}, 'native empty PC authoritative')
|
||||
saved(path); App.unload()
|
||||
source = native(); source.bag.pockets.ITEMS = {}
|
||||
for i = 1, 42 do source.bag.pockets.ITEMS[i] = { id = 2000 + i, qty = 1 } end
|
||||
S, path = load(source)
|
||||
local before = Copy(S.save)
|
||||
assert(not Ops.addToBag(S, 68)); eq(S.save, before, 'full pocket rollback'); eq(S.dirty, false, 'full rejection dirty')
|
||||
App.unload()
|
||||
source = native(); source.storage = nil
|
||||
source.pc = { items = { { id = 68, qty = 120 } }, mons = { mon(9) }, custom = true }
|
||||
S, path = load(source); assert(Ops.pcAdjust(S, 68, -1))
|
||||
local output = saved(path)
|
||||
eq(output.storage.items[1].qty, 119, 'legacy PC migration quantity')
|
||||
eq(output.storage.boxes[1].mons[1].species, 9, 'legacy PC mons retained')
|
||||
eq(output.pc.custom, true, 'legacy unknown field')
|
||||
App.unload()
|
||||
print('PASS native_empty_legacy_full_pocket')
|
||||
end
|
||||
do
|
||||
local A = require('Game3Adapter')
|
||||
local source = native()
|
||||
local old = mon(6); old.nickname = 'OLDDEPOSIT'
|
||||
source.boxes = { { [7] = old, [8] = Copy(source.storage.boxes[1].mons[7]) } }
|
||||
local S, path = load(source)
|
||||
local function count(save, nickname)
|
||||
local n = 0
|
||||
for _, box in pairs(save.storage.boxes) do
|
||||
for _, m in pairs(box.mons) do if m.nickname == nickname then n = n + 1 end end
|
||||
end
|
||||
return n
|
||||
end
|
||||
eq(S.save.storage.boxes[1].mons[7].species, 9, 'collision native preserved')
|
||||
eq(count(S.save, 'OLDDEPOSIT'), 1, 'collision deposit relocated')
|
||||
eq(count(S.save, 'KEPT'), 1, 'serialized mirror deduplicated')
|
||||
local before = Copy(S.save)
|
||||
Gen.ensureBoxes(S.save); Gen.hydrateSave(S.data, S.save)
|
||||
eq(S.save, before, 'legacy coexistence repeated hydration')
|
||||
local output = saved(path)
|
||||
eq(count(output, 'OLDDEPOSIT'), 1, 'old editor no-op save retains deposit')
|
||||
eq(count(Schema.fromSaveTable(output), 'OLDDEPOSIT'), 1, 'runtime retains old deposit')
|
||||
App.unload(); App.load(path, { version = 'firered', embedded = true })
|
||||
eq(count(saved(path), 'OLDDEPOSIT'), 1, 'second no-op save no duplication')
|
||||
App.unload()
|
||||
source = native()
|
||||
for b = 1, 14 do
|
||||
source.storage.boxes[b] = source.storage.boxes[b] or { mons = {} }
|
||||
for s = 1, 30 do source.storage.boxes[b].mons[s] = mon(9) end
|
||||
end
|
||||
source.boxes = { { old } }
|
||||
before = Copy(source)
|
||||
local ok, err = pcall(A.ensureStorage, source)
|
||||
assert(not ok and tostring(err):find('native storage is full', 1, true))
|
||||
eq(source, before, 'full collision leaves all recoverable data intact')
|
||||
S, path = load(source)
|
||||
assert(S.loadError and not S.allowSave and not App.save(), 'full collision disables App save')
|
||||
eq(assert(IO.load(path)), source, 'failed App load leaves file intact')
|
||||
App.unload()
|
||||
source.storage.boxes[14].mons[30] = nil
|
||||
S, path = load(source)
|
||||
eq(count(saved(path), 'OLDDEPOSIT'), 1, 'last free slot collision migration')
|
||||
App.unload()
|
||||
print('PASS legacy_native_boxes_collision_noop_full_storage')
|
||||
end
|
||||
|
||||
do
|
||||
local A = require('Game3Adapter')
|
||||
for _, mode in ipairs({ 'minus', 'drop', 'max' }) do
|
||||
local source = native()
|
||||
local slots = { { id = 13, qty = 700, custom = 'before' }, { id = 68, qty = 120, custom = 'target' },
|
||||
{ id = 2001, qty = 77, custom = 'middle' }, { id = 'RARE_CANDY', qty = 1 },
|
||||
{ id = '68', qty = 2 }, { id = 2002, qty = 88, custom = 'after' } }
|
||||
source.bag.pockets.ITEMS, source.storage.items = Copy(slots), Copy(slots)
|
||||
local S, path = load(source)
|
||||
eq(S.save.inventory['68'], 123, 'duplicate bag total')
|
||||
eq(S.save.pcItems['68'], 123, 'duplicate PC total')
|
||||
local untouched = Copy(S.save)
|
||||
assert(not A.change(S.data, S.save, false, { { id = 68, qty = 122 }, { id = 13, qty = 1000 } }))
|
||||
eq(S.save, untouched, 'duplicate bag batch rejection rollback')
|
||||
assert(not A.change(S.data, S.save, true, { { id = 68, qty = 122 }, { id = 13, qty = 1000 } }))
|
||||
eq(S.save, untouched, 'duplicate PC batch rejection rollback')
|
||||
local expected = mode == 'minus' and 122 or mode == 'max' and 999 or 0
|
||||
for _, prefix in ipairs({ 'bag', 'pc' }) do
|
||||
if mode == 'minus' then assert(Ops[prefix .. 'Adjust'](S, 'RARE_CANDY', -1))
|
||||
elseif mode == 'drop' then assert(Ops[prefix .. 'Drop'](S, '68'))
|
||||
else assert(Ops[prefix .. 'Max'](S, 68)) end
|
||||
end
|
||||
local expectedSlots = { Copy(slots[1]), Copy(slots[3]), Copy(slots[6]) }
|
||||
if expected > 0 then
|
||||
local target = Copy(slots[2]); target.qty = expected
|
||||
table.insert(expectedSlots, 2, target)
|
||||
end
|
||||
eq(S.save.bag.pockets.ITEMS, expectedSlots, 'bag duplicate ' .. mode)
|
||||
eq(S.save.storage.items, expectedSlots, 'PC duplicate ' .. mode)
|
||||
local output = saved(path)
|
||||
eq(output.bag.pockets.ITEMS, expectedSlots, 'persist bag duplicate ' .. mode)
|
||||
eq(output.storage.items, expectedSlots, 'persist PC duplicate ' .. mode)
|
||||
App.unload(); App.load(path, { version = 'firered', embedded = true }); S = App.getState()
|
||||
eq(S.save.inventory['68'] or 0, expected, 'reload bag aggregate ' .. mode)
|
||||
eq(S.save.pcItems['68'] or 0, expected, 'reload PC aggregate ' .. mode)
|
||||
if mode == 'max' then
|
||||
untouched = Copy(S.save)
|
||||
assert(not Ops.addToBag(S, 68) and not Ops.addToPc(S, 'RARE_CANDY'))
|
||||
eq(S.save, untouched, 'duplicate capped rejection unchanged')
|
||||
end
|
||||
App.unload()
|
||||
end
|
||||
print('PASS duplicate_bag_pc_alias_minus_drop_max_reload_rollback')
|
||||
end
|
||||
do
|
||||
local source = native()
|
||||
local S, path = load(source)
|
||||
S.editingMon, S.dirty, S._quitArmed, S._openArmed = S.save.party[1], true, true, true
|
||||
assert(App.reload())
|
||||
eq(S.editingMon, nil, 'reload clears mon editor')
|
||||
eq(S.dirty, false, 'reload clears dirty')
|
||||
eq(S._quitArmed, false, 'reload disarms quit')
|
||||
eq(S._openArmed, false, 'reload disarms open')
|
||||
eq(S.save.inventory['68'], 2, 'actual reload bag projection')
|
||||
eq(S.save.pcItems['68'], 120, 'actual reload PC projection')
|
||||
assert(S.save.boxes[1] == S.save.storage.boxes[1].mons, 'reload native box alias')
|
||||
local output = saved(path)
|
||||
eq(output.bag.pockets, source.bag.pockets, 'reload no-op bag')
|
||||
eq(output.storage.items, source.storage.items, 'reload no-op PC')
|
||||
eq(output.storage.boxes[1].mons[7].species, 9, 'reload no-op native sparse mon')
|
||||
assert(App.reload())
|
||||
assert(Ops.addToBag(S, 'RARE_CANDY'))
|
||||
assert(Ops.pcAdjust(S, '68', -1))
|
||||
S.selectedParty, S.selectedBox, S.selectedBoxSlot = 2, 1, 2
|
||||
assert(Ops.deposit(S))
|
||||
output = saved(path)
|
||||
local runtime = Schema.fromSaveTable(Copy(output))
|
||||
eq(Bag.get(runtime.bag, 68), 3, 'reload subsequent bag edit')
|
||||
eq(runtime.storage.items[2].qty, 119, 'reload subsequent PC edit')
|
||||
eq(runtime.storage.boxes[1].mons[2].species, 6, 'reload subsequent deposit')
|
||||
eq(runtime.storage.boxes[1].mons[7].species, 9, 'reload untouched native mon')
|
||||
for b = 1, 14 do
|
||||
source.storage.boxes[b] = source.storage.boxes[b] or { mons = {} }
|
||||
for s = 1, 30 do source.storage.boxes[b].mons[s] = mon(9) end
|
||||
end
|
||||
source.boxes = { { mon(6) } }
|
||||
local f = assert(io.open(path, 'wb')); f:write(SD.encode(source)); f:close()
|
||||
assert(not App.reload() and S.loadError and not S.allowSave, 'reload collision disables save')
|
||||
assert(not App.save())
|
||||
eq(assert(IO.load(path)), source, 'reload collision leaves disk intact')
|
||||
App.unload()
|
||||
print('PASS actual_app_reload_native_noop_edits_collision')
|
||||
end
|
||||
for _, path in ipairs(files) do os.remove(path) end
|
||||
print('PASS save_editor_gen3_native_persistence')
|
||||
@@ -149,8 +149,8 @@ do
|
||||
|
||||
-- Set Move
|
||||
MonOps.setMove(mockData, mon, 1, 57) -- Surf
|
||||
checkEq(mon.moves[1].moveId, 57, "move 1 set to Surf (57)")
|
||||
check(mon.moves[1].pp > 0, "move 1 PP initialized")
|
||||
checkEq(require("src.core.game3.pokemon").moveIdAt(mon, 1), 57, "move 1 set to Surf (57)")
|
||||
check(mon.pp[1] > 0, "move 1 PP initialized")
|
||||
|
||||
-- Set DVs / IVs
|
||||
MonOps.setDv(mockData, mon, "attack", 15, 3)
|
||||
@@ -339,7 +339,7 @@ do
|
||||
ivs = { hp = 15, atk = 14, def = 13, spe = 12, spa = 11, spd = 10 },
|
||||
}
|
||||
Gen.hydrateMon(mockData, rawMon)
|
||||
checkEq(rawMon.species, "BULBASAUR", "hydrateMon normalizes numeric species to BULBASAUR")
|
||||
checkEq(rawMon.species, 1, "hydrateMon preserves native numeric species")
|
||||
checkEq(rawMon.speciesId, 1, "hydrateMon preserves speciesId = 1")
|
||||
check(type(rawMon.dvs) == "table", "hydrateMon creates dvs table")
|
||||
checkEq(rawMon.dvs.attack, 7, "hydrateMon computes attack DV from IV")
|
||||
@@ -368,7 +368,7 @@ do
|
||||
|
||||
-- Test MonOps.setSpecies
|
||||
Ops.setSpecies(S, mon, "CHARMANDER")
|
||||
checkEq(mon.species, "CHARMANDER", "Ops.setSpecies updates to CHARMANDER")
|
||||
checkEq(mon.species, 4, "Ops.setSpecies updates to numeric CHARMANDER")
|
||||
checkEq(mon.speciesId, 4, "Ops.setSpecies updates speciesId to 4")
|
||||
end
|
||||
|
||||
@@ -440,31 +440,31 @@ do
|
||||
|
||||
-- Add to bag
|
||||
Ops.addToBag(S, "POTION")
|
||||
checkEq(save.inventory["POTION"], 1, "POTION added to bag x1")
|
||||
checkEq(save.inventory["13"], 1, "POTION added to bag x1")
|
||||
check(save.bag ~= nil and save.bag.stacks ~= nil, "save.bag updated")
|
||||
|
||||
-- Adjust bag quantity
|
||||
Ops.bagAdjust(S, "POTION", 4)
|
||||
checkEq(save.inventory["POTION"], 5, "POTION adjusted to x5")
|
||||
checkEq(save.inventory["13"], 5, "POTION adjusted to x5")
|
||||
|
||||
-- Max bag stack
|
||||
Ops.bagMax(S, "POTION")
|
||||
checkEq(save.inventory["POTION"], Ops.STACK_MAX, "POTION maxed to 99")
|
||||
checkEq(save.inventory["13"], 999, "POTION maxed to 999")
|
||||
|
||||
-- Drop bag item
|
||||
Ops.bagDrop(S, "POTION")
|
||||
checkEq(save.inventory["POTION"], nil, "POTION dropped from bag")
|
||||
checkEq(save.inventory["13"], nil, "POTION dropped from bag")
|
||||
|
||||
-- PC operations
|
||||
Ops.addToPc(S, "POKE_BALL")
|
||||
checkEq(save.pcItems["POKE_BALL"], 1, "POKE_BALL added to PC x1")
|
||||
checkEq(save.pcItems["4"], 1, "POKE_BALL added to PC x1")
|
||||
check(save.storage ~= nil and #save.storage.items > 0, "save.storage.items synced")
|
||||
|
||||
Ops.pcAdjust(S, "POKE_BALL", 9)
|
||||
checkEq(save.pcItems["POKE_BALL"], 10, "POKE_BALL adjusted to x10 in PC")
|
||||
checkEq(save.pcItems["4"], 10, "POKE_BALL adjusted to x10 in PC")
|
||||
|
||||
Ops.pcMax(S, "POKE_BALL")
|
||||
checkEq(save.pcItems["POKE_BALL"], Ops.STACK_MAX, "POKE_BALL maxed in PC")
|
||||
checkEq(save.pcItems["4"], 999, "POKE_BALL maxed in PC")
|
||||
|
||||
-- Test numeric item IDs in Bag.order, Bag.slots, and isBadge
|
||||
save.inventory[13] = 5 -- Potion numeric ID
|
||||
@@ -551,7 +551,7 @@ do
|
||||
|
||||
-- Test table-typed entries in save.inventory and save.pcItems
|
||||
S.save.inventory[1] = { id = 13, qty = 5 }
|
||||
S.save.inventory["POTION"] = { qty = 10 }
|
||||
S.save.inventory["13"] = { qty = 10 }
|
||||
S.save.pcItems[1] = { id = 4, qty = 20 }
|
||||
check(pcall(Ops.bagCanMax, S), "Ops.bagCanMax handles table entries in inventory without error")
|
||||
check(pcall(Ops.pcCanMax, S), "Ops.pcCanMax handles table entries in pcItems without error")
|
||||
|
||||
@@ -121,8 +121,15 @@ local function applyLoaded(path, statusVerb)
|
||||
S._openArmed = false
|
||||
S.editingMon = nil
|
||||
Ops.disarm(S)
|
||||
Gen.ensureBoxes(S.save)
|
||||
Gen.hydrateSave(Data, S.save)
|
||||
local prepared, prepareError = pcall(function()
|
||||
Gen.ensureBoxes(S.save)
|
||||
Gen.hydrateSave(Data, S.save)
|
||||
end)
|
||||
if not prepared then
|
||||
S.loadError, S.allowSave = true, false
|
||||
S.status = "Save disabled: " .. tostring(prepareError)
|
||||
return
|
||||
end
|
||||
local probe = require("src.mods.Merge").deepCopy(S.save)
|
||||
S.validation = Gen.validate(probe, Data)
|
||||
if not Gen.emptyReport(S.save, S.validation) then
|
||||
@@ -290,7 +297,13 @@ function App.save()
|
||||
if not S.allowSave then
|
||||
return Ops.say(S, "Save disabled, corrupt save loaded; fix the file and Reload first")
|
||||
end
|
||||
local ok, err = SaveIO.save(S.path, S.save)
|
||||
local output = S.save
|
||||
if Gen.ofState(S) == 3 then
|
||||
local prepared, result = pcall(require("Game3Adapter").export, S.save)
|
||||
if not prepared then S.status = "Save failed: " .. tostring(result); return false end
|
||||
output = result
|
||||
end
|
||||
local ok, err = SaveIO.save(S.path, output)
|
||||
if ok then
|
||||
S.dirty = false
|
||||
S._quitArmed = false
|
||||
@@ -305,6 +318,10 @@ end
|
||||
function App.reload()
|
||||
local save, err = SaveIO.load(S.path)
|
||||
if save then
|
||||
if Gen.of(save, S.version) == 3 then
|
||||
applyLoaded(S.path, "Reloaded")
|
||||
return not S.loadError
|
||||
end
|
||||
S.save = save
|
||||
S.dirty = false
|
||||
S.loadError = false
|
||||
@@ -594,7 +611,7 @@ local function tabCount(id)
|
||||
return ("%d/%d"):format(#S.save.party, require("src.pokemon.Party").MAX)
|
||||
elseif id == "boxes" then
|
||||
local n = 0
|
||||
for _, box in ipairs(Ops.boxes(S)) do n = n + #box end
|
||||
for _, box in ipairs(Ops.boxes(S)) do n = n + Ops.boxSize(S, box) end
|
||||
return tostring(n)
|
||||
elseif id == "items" then
|
||||
local Bag = require("src.inventory.Bag")
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
local Copy = require("src.mods.Merge").deepCopy
|
||||
local Items = require("src.core.game3.items_data")
|
||||
local Bag = require("src.core.game3.bag")
|
||||
local Storage = require("src.core.game3.storage")
|
||||
local Mons = require("src.core.game3.save_mon")
|
||||
|
||||
local M = {}
|
||||
local pockets = { "ITEMS", "KEY_ITEMS", "POKE_BALLS", "TM_CASE", "BERRY_POUCH" }
|
||||
|
||||
function M.itemId(data, id)
|
||||
local def = data and data.items and data.items[id]
|
||||
return tonumber(def and def.itemId) or Items.toNumericId(id) or id
|
||||
end
|
||||
|
||||
function M.key(data, id)
|
||||
return tostring(M.itemId(data, id))
|
||||
end
|
||||
|
||||
local function equal(a, b)
|
||||
if a == b then return true end
|
||||
if type(a) ~= "table" or type(b) ~= "table" then return false end
|
||||
for k, v in pairs(a) do if not equal(v, b[k]) then return false end end
|
||||
for k in pairs(b) do if a[k] == nil then return false end end
|
||||
return true
|
||||
end
|
||||
|
||||
function M.ensureStorage(save)
|
||||
local legacy = type(save.storage) ~= "table"
|
||||
local source = legacy and Storage.restore(nil, save.pc, save.pcItems or save.pc_items) or save.storage
|
||||
local storage = {}
|
||||
for k, v in pairs(source) do storage[k] = v end
|
||||
storage.boxes, storage.items = {}, source.items or {}
|
||||
local boxes = {}
|
||||
local available = {}
|
||||
for b = 1, 14 do
|
||||
local original = source.boxes and source.boxes[b] or { name = "BOX " .. b, wallpaper = ((b - 1) % 16) + 1 }
|
||||
local box = {}
|
||||
for k, v in pairs(original) do box[k] = v end
|
||||
box.mons = {}
|
||||
for s, mon in pairs(original.mons or {}) do
|
||||
box.mons[s] = mon
|
||||
available[#available + 1] = { mon = mon }
|
||||
end
|
||||
storage.boxes[b], boxes[b] = box, box.mons
|
||||
end
|
||||
for b, box in pairs(save.boxes or {}) do
|
||||
for s, mon in pairs(type(box) == "table" and box or {}) do
|
||||
if type(s) == "number" and type(mon) == "table" then
|
||||
local mirrored = false
|
||||
for _, entry in ipairs(available) do
|
||||
if not entry.used and equal(entry.mon, mon) then entry.used = true; mirrored = true; break end
|
||||
end
|
||||
if not mirrored then
|
||||
local targetB, targetS
|
||||
if boxes[b] and s >= 1 and s <= 30 and s == math.floor(s) and not boxes[b][s] then
|
||||
targetB, targetS = b, s
|
||||
else
|
||||
targetB, targetS = Storage.findOpenSlot(storage)
|
||||
end
|
||||
if not targetB then error("Cannot preserve legacy box Pokemon: native storage is full") end
|
||||
boxes[targetB][targetS] = mon
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
save.storage = storage
|
||||
if legacy and type(save.pc) == "table" then save.pc.mons, save.pc.items = nil, nil end
|
||||
save.boxes = boxes
|
||||
save.currentBox = math.max(1, math.min(14, tonumber(storage.currentBox) or 1))
|
||||
return boxes
|
||||
end
|
||||
|
||||
local function project(data, slots, out, order)
|
||||
for _, slot in ipairs(slots or {}) do
|
||||
if slot.id ~= nil and (tonumber(slot.qty) or 0) > 0 then
|
||||
local key = M.key(data, slot.id)
|
||||
if not out[key] and order then order[#order + 1] = key end
|
||||
out[key] = (out[key] or 0) + slot.qty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.project(data, save)
|
||||
local inv, order, pc = {}, {}, {}
|
||||
for _, pocket in ipairs(pockets) do project(data, save.bag.pockets[pocket], inv, order) end
|
||||
project(data, save.storage.items, pc)
|
||||
save.inventory, save.bagOrder, save.pcItems = inv, order, pc
|
||||
save.bag.stacks = Copy(inv)
|
||||
end
|
||||
|
||||
function M.hydrate(data, save)
|
||||
M.ensureStorage(save)
|
||||
if type(save.bag) ~= "table" then save.bag = Copy(save.inventory or {}) end
|
||||
if type(save.bag.pockets) ~= "table" then
|
||||
local source = save.bag
|
||||
if not source.stacks and not source.items then source = { stacks = source } end
|
||||
save.bag = Bag.migrate(Copy(source))
|
||||
end
|
||||
M.project(data, save)
|
||||
end
|
||||
|
||||
function M.export(save)
|
||||
local out = Copy(save)
|
||||
M.ensureStorage(out)
|
||||
Mons.each(out, Mons.normalize)
|
||||
out.inventory = Copy(out.bag)
|
||||
out.boxes, out.pcItems, out.bagOrder = nil, nil, nil
|
||||
return out
|
||||
end
|
||||
|
||||
local function locate(data, slots, id)
|
||||
for i, slot in ipairs(slots) do
|
||||
if M.key(data, slot.id) == M.key(data, id) then return i, slot end
|
||||
end
|
||||
end
|
||||
|
||||
function M.quantity(data, save, pc, id)
|
||||
local key = M.key(data, id)
|
||||
return tonumber((pc and save.pcItems or save.inventory or {})[key]) or 0
|
||||
end
|
||||
|
||||
local function set(data, save, pc, id, qty)
|
||||
if type(qty) ~= "number" or qty ~= math.floor(qty) or qty < 0 or qty > 999 then return false end
|
||||
id = M.itemId(data, id)
|
||||
local slots, cap
|
||||
if pc then
|
||||
slots, cap = save.storage.items, Storage.PC_ITEMS_COUNT
|
||||
else
|
||||
for _, p in ipairs(pockets) do
|
||||
local list = save.bag.pockets[p]
|
||||
if list and locate(data, list, id) then slots = list; break end
|
||||
end
|
||||
local pocket = Items.pocketOf(id)
|
||||
slots = slots or save.bag.pockets[pocket]
|
||||
if not slots then slots = {}; save.bag.pockets[pocket] = slots end
|
||||
cap = Items.CAPACITY[pocket] or 42
|
||||
end
|
||||
local lists = pc and { slots } or {}
|
||||
if not pc then for _, p in ipairs(pockets) do lists[#lists + 1] = save.bag.pockets[p] or {} end end
|
||||
local found = false
|
||||
for _, list in ipairs(lists) do
|
||||
local i = 1
|
||||
while i <= #list do
|
||||
local slot = list[i]
|
||||
if M.key(data, slot.id) == M.key(data, id) then
|
||||
if not found and qty > 0 then
|
||||
slot.qty = qty
|
||||
found = true
|
||||
i = i + 1
|
||||
else
|
||||
found = true
|
||||
table.remove(list, i)
|
||||
end
|
||||
else i = i + 1 end
|
||||
end
|
||||
end
|
||||
if found then
|
||||
return true
|
||||
elseif qty > 0 then
|
||||
if #slots >= cap then return false end
|
||||
if not pc then
|
||||
local pocket = Items.pocketOf(id)
|
||||
local container = pocket == "TM_CASE" and Items.ITEM_TM_CASE or pocket == "BERRY_POUCH" and Items.ITEM_BERRY_POUCH
|
||||
if container and not locate(data, save.bag.pockets.KEY_ITEMS or {}, container) then
|
||||
if not set(data, save, false, container, 1) then return false end
|
||||
end
|
||||
end
|
||||
slots[#slots + 1] = { id = id, qty = qty }
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function M.change(data, save, pc, changes)
|
||||
local staged = { bag = pc and save.bag or Copy(save.bag), storage = save.storage }
|
||||
if pc then
|
||||
staged.storage = {}
|
||||
for k, value in pairs(save.storage) do staged.storage[k] = value end
|
||||
staged.storage.items = Copy(save.storage.items)
|
||||
end
|
||||
for _, change in ipairs(changes) do
|
||||
if not set(data, staged, pc, change.id, change.qty) then return false end
|
||||
end
|
||||
save.bag, save.storage = staged.bag, staged.storage
|
||||
M.ensureStorage(save)
|
||||
M.project(data, save)
|
||||
return true
|
||||
end
|
||||
|
||||
return M
|
||||
+11
-111
@@ -216,6 +216,7 @@ function Gen.bindGame3Data(data)
|
||||
local iDef = { id = normName, name = info.name, pocket = info.pocket, itemId = num }
|
||||
data.items[normName] = iDef
|
||||
data.items[num] = iDef
|
||||
data.items[tostring(num)] = iDef
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -267,13 +268,15 @@ function Gen.hydrateMon(data, mon)
|
||||
if isG3 then
|
||||
local okP, Pokemon = pcall(require, "src.core.game3.pokemon")
|
||||
if okP and Pokemon then
|
||||
local spId = tonumber(mon.speciesId or mon.species)
|
||||
or (Pokemon.speciesFromName and Pokemon.speciesFromName(tostring(mon.species)))
|
||||
require("src.core.game3.save_mon").normalize(mon)
|
||||
local spId = Pokemon.speciesOf(mon)
|
||||
if spId then
|
||||
mon.speciesId = spId
|
||||
local name = Pokemon.name(spId)
|
||||
if name and name ~= "" and name ~= "??????????" then
|
||||
mon.species = name
|
||||
mon.species = spId
|
||||
mon.name = mon.name or name
|
||||
mon.speciesNumbering = Pokemon.NUMBERING_INTERNAL
|
||||
end
|
||||
end
|
||||
mon.ivs = mon.ivs or { hp = 0, atk = 0, def = 0, spe = 0, spa = 0, spd = 0 }
|
||||
@@ -285,17 +288,7 @@ function Gen.hydrateMon(data, mon)
|
||||
special = math.floor(((mon.ivs.spa or 0) + (mon.ivs.spd or 0)) / 4),
|
||||
hp = math.floor((mon.ivs.hp or 0) / 2),
|
||||
}
|
||||
Pokemon.applyStats(mon)
|
||||
mon.stats = {
|
||||
hp = mon.maxHp or mon.hp or 10,
|
||||
attack = mon.attack or 10,
|
||||
defense = mon.defense or 10,
|
||||
speed = mon.speed or 10,
|
||||
spAtk = mon.spAtk or mon.spa or 10,
|
||||
spDef = mon.spDef or mon.spd or 10,
|
||||
specialAttack = mon.spAtk or mon.spa or 10,
|
||||
specialDefense = mon.spDef or mon.spd or 10,
|
||||
}
|
||||
require("src.core.game3.save_mon").normalize(mon)
|
||||
end
|
||||
return mon
|
||||
end
|
||||
@@ -315,97 +308,9 @@ function Gen.hydrateSave(data, save)
|
||||
if type(save) ~= "table" then return save end
|
||||
local g = Gen.of(save)
|
||||
if g == 3 then
|
||||
save.inventory = save.inventory or {}
|
||||
save.bagOrder = save.bagOrder or {}
|
||||
save.pcItems = save.pcItems or {}
|
||||
save.bag = save.bag or require("src.core.game3.bag").new()
|
||||
|
||||
if save.inventory then
|
||||
local arraySlots = {}
|
||||
for k, v in pairs(save.inventory) do
|
||||
if type(k) == "number" and type(v) == "table" then
|
||||
arraySlots[#arraySlots + 1] = k
|
||||
local sId = v.id or v.itemId or v.name or v[1]
|
||||
local sQty = tonumber(v.qty or v.quantity or v.count or v[2]) or 1
|
||||
if sId and sQty > 0 then
|
||||
local okD, ItemsData = pcall(require, "src.core.game3.items_data")
|
||||
local okI, Items = pcall(require, "src.core.game3.items")
|
||||
local num = okD and ItemsData and ItemsData.toNumericId(sId)
|
||||
local name = (num and okI and Items and Items.FRLG_TO_HOST[num]) or (okD and ItemsData and ItemsData.bagKey(sId)) or tostring(sId)
|
||||
save.inventory[name] = sQty
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, k in ipairs(arraySlots) do
|
||||
save.inventory[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if save.bag and save.bag.pockets then
|
||||
local okD, ItemsData = pcall(require, "src.core.game3.items_data")
|
||||
local okI, Items = pcall(require, "src.core.game3.items")
|
||||
if okD and ItemsData and okI and Items then
|
||||
for _, pocketList in pairs(save.bag.pockets) do
|
||||
for _, slot in ipairs(pocketList or {}) do
|
||||
if slot.id and (tonumber(slot.qty) or 0) > 0 then
|
||||
local num = ItemsData.toNumericId(slot.id)
|
||||
local name = (num and Items.FRLG_TO_HOST[num]) or ItemsData.bagKey(slot.id) or tostring(slot.id)
|
||||
save.inventory[name] = tonumber(slot.qty) or 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if save.pcItems then
|
||||
local arraySlots = {}
|
||||
for k, v in pairs(save.pcItems) do
|
||||
if type(k) == "number" and type(v) == "table" then
|
||||
arraySlots[#arraySlots + 1] = k
|
||||
local sId = v.id or v.itemId or v.name or v[1]
|
||||
local sQty = tonumber(v.qty or v.quantity or v.count or v[2]) or 1
|
||||
if sId and sQty > 0 then
|
||||
local okD, ItemsData = pcall(require, "src.core.game3.items_data")
|
||||
local okI, Items = pcall(require, "src.core.game3.items")
|
||||
local num = okD and ItemsData and ItemsData.toNumericId(sId)
|
||||
local name = (num and okI and Items and Items.FRLG_TO_HOST[num]) or (okD and ItemsData and ItemsData.bagKey(sId)) or tostring(sId)
|
||||
save.pcItems[name] = sQty
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, k in ipairs(arraySlots) do
|
||||
save.pcItems[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
local pcLists = {}
|
||||
if type(save.storage) == "table" and type(save.storage.items) == "table" then
|
||||
pcLists[1] = save.storage.items
|
||||
elseif type(save.pc) == "table" and type(save.pc.items) == "table" then
|
||||
pcLists[1] = save.pc.items
|
||||
end
|
||||
for _, pcList in ipairs(pcLists) do
|
||||
local okD, ItemsData = pcall(require, "src.core.game3.items_data")
|
||||
local okI, Items = pcall(require, "src.core.game3.items")
|
||||
if okD and ItemsData and okI and Items then
|
||||
for _, slot in ipairs(pcList) do
|
||||
local id = slot.id or slot.itemId
|
||||
local qty = slot.qty or slot.quantity or 1
|
||||
if id and (tonumber(qty) or 0) > 0 then
|
||||
local num = ItemsData.toNumericId(id)
|
||||
local name = (num and Items.FRLG_TO_HOST[num]) or ItemsData.bagKey(id) or tostring(id)
|
||||
save.pcItems[name] = tonumber(qty) or 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, mon in ipairs(save.party or {}) do Gen.hydrateMon(data, mon) end
|
||||
for _, box in ipairs(save.boxes or {}) do
|
||||
if type(box) == "table" then
|
||||
for _, mon in ipairs(box) do Gen.hydrateMon(data, mon) end
|
||||
end
|
||||
end
|
||||
require("Game3Adapter").hydrate(data, save)
|
||||
local Mons = require("src.core.game3.save_mon")
|
||||
Mons.each(save, function(mon) Mons.normalize(mon); Gen.hydrateMon(data, mon) end)
|
||||
return save
|
||||
elseif g == 2 then
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
@@ -427,12 +332,7 @@ end
|
||||
function Gen.ensureBoxes(save)
|
||||
local g = Gen.of(save)
|
||||
if g == 3 then
|
||||
save.boxes = save.boxes or {}
|
||||
for i = 1, 14 do
|
||||
save.boxes[i] = save.boxes[i] or {}
|
||||
end
|
||||
save.currentBox = math.max(1, math.min(14, save.currentBox or 1))
|
||||
return save.boxes
|
||||
return require("Game3Adapter").ensureStorage(save)
|
||||
elseif g == 2 then
|
||||
local Boxes2 = require("src.core.gen2.Boxes")
|
||||
save.boxes = save.boxes or {}
|
||||
|
||||
@@ -39,7 +39,7 @@ function MonOps.create(data, species, level, gen)
|
||||
if mvId and mvId > 0 then
|
||||
local mvName = PokemonG3.moveName(mvId)
|
||||
monMoves[slot] = {
|
||||
id = mvName,
|
||||
id = mvId,
|
||||
moveId = mvId,
|
||||
pp = pp[slot] or 10,
|
||||
maxPp = maxPp[slot] or 10,
|
||||
@@ -48,7 +48,7 @@ function MonOps.create(data, species, level, gen)
|
||||
end
|
||||
|
||||
local mon = {
|
||||
species = name,
|
||||
species = spId,
|
||||
speciesId = spId,
|
||||
name = name,
|
||||
nickname = "",
|
||||
@@ -74,7 +74,7 @@ function MonOps.create(data, species, level, gen)
|
||||
pokeball = 4, -- pokefirered/src/pokemon.c:1820
|
||||
}
|
||||
|
||||
PokemonG3.applyStats(mon)
|
||||
require("src.core.game3.save_mon").normalize(mon)
|
||||
mon.stats = {
|
||||
hp = mon.maxHp,
|
||||
attack = mon.attack,
|
||||
@@ -102,17 +102,7 @@ function MonOps.recalc(data, mon, gen)
|
||||
if isG3 then
|
||||
local okP, PokemonG3 = pcall(require, "src.core.game3.pokemon")
|
||||
if okP and PokemonG3 then
|
||||
PokemonG3.applyStats(mon)
|
||||
mon.stats = {
|
||||
hp = mon.maxHp or mon.hp or 10,
|
||||
attack = mon.attack or 10,
|
||||
defense = mon.defense or 10,
|
||||
speed = mon.speed or 10,
|
||||
spAtk = mon.spAtk or mon.spa or 10,
|
||||
spDef = mon.spDef or mon.spd or 10,
|
||||
specialAttack = mon.spAtk or mon.spa or 10,
|
||||
specialDefense = mon.spDef or mon.spd or 10,
|
||||
}
|
||||
require("src.core.game3.save_mon").normalize(mon)
|
||||
mon.hp = math.max(0, math.min(mon.hp or mon.maxHp, mon.maxHp))
|
||||
end
|
||||
return
|
||||
@@ -135,7 +125,7 @@ function MonOps.setLevel(data, mon, level, gen)
|
||||
|
||||
if isG3 then
|
||||
local SummaryData = require("src.core.game3.summary_data")
|
||||
local gr = mon.growthRate or (data and data.pokemon and data.pokemon[mon.species] and data.pokemon[mon.species].growthRate) or 0
|
||||
local gr = require("src.core.game3.pokemon").growthRate(require("src.core.game3.pokemon").speciesOf(mon)) or mon.growthRate or 0
|
||||
mon.exp = SummaryData.expForLevel(gr, level)
|
||||
mon.experience = mon.exp
|
||||
MonOps.recalc(data, mon, gen)
|
||||
@@ -154,6 +144,16 @@ function MonOps.setLevel(data, mon, level, gen)
|
||||
MonOps.recalc(data, mon, gen)
|
||||
end
|
||||
|
||||
function MonOps.clearMove(mon, slot)
|
||||
for _, key in ipairs({ "moves", "pp", "maxPp", "moveIds", "ppBonuses", "ppBonus", "ppUp" }) do
|
||||
if type(mon[key]) == "table" then mon[key][slot] = nil end
|
||||
end
|
||||
if type(mon.ppBonusesPacked) == "number" then
|
||||
local bit = require("bit")
|
||||
mon.ppBonusesPacked = bit.band(mon.ppBonusesPacked, bit.bnot(bit.lshift(3, (slot - 1) * 2)))
|
||||
end
|
||||
end
|
||||
|
||||
function MonOps.setMove(data, mon, slot, moveId)
|
||||
assert(slot >= 1 and slot <= 4)
|
||||
local mdef = data and data.moves and data.moves[moveId]
|
||||
@@ -175,6 +175,14 @@ function MonOps.setMove(data, mon, slot, moveId)
|
||||
assert(type(mdef) == "table", "unknown move: " .. tostring(moveId))
|
||||
mon.moves = mon.moves or {}
|
||||
local basePp = mdef.pp or 10
|
||||
if mon.speciesId ~= nil or mon.personality ~= nil then
|
||||
local nativeId = assert(tonumber(mdef.moveId or numMove), "unknown native move")
|
||||
MonOps.clearMove(mon, slot)
|
||||
mon.moves[slot] = nativeId
|
||||
mon.pp, mon.maxPp = mon.pp or {}, mon.maxPp or {}
|
||||
mon.pp[slot], mon.maxPp[slot] = basePp, basePp
|
||||
return
|
||||
end
|
||||
local currentUps = (mon.moves[slot] and mon.moves[slot].ppUps) or 0
|
||||
mon.moves[slot] = {
|
||||
id = mdef.id or mdef.name or tostring(moveId),
|
||||
@@ -234,7 +242,8 @@ function MonOps.setSpecies(data, mon, species, gen)
|
||||
pcall(PokemonG3.install, nil)
|
||||
local spId = tonumber(species) or (PokemonG3.speciesFromName and PokemonG3.speciesFromName(tostring(species))) or 1
|
||||
local name = (PokemonG3.name and PokemonG3.name(spId)) or tostring(species)
|
||||
mon.species = name
|
||||
mon.species = spId
|
||||
mon.speciesNumbering = PokemonG3.NUMBERING_INTERNAL
|
||||
mon.speciesId = spId
|
||||
mon.name = name
|
||||
local meta = PokemonG3.speciesMeta and PokemonG3.speciesMeta(spId)
|
||||
|
||||
+137
-137
@@ -20,9 +20,19 @@ local Charmap = require("src.save_convert.data.charmap")
|
||||
local Gen = require("Gen")
|
||||
|
||||
local Ops = {}
|
||||
local G3 = require("Game3Adapter")
|
||||
local boxInsert, boxRemove
|
||||
|
||||
Ops.MONEY_MAX = 999999
|
||||
Ops.STACK_MAX = 99
|
||||
function Ops.stackMax(S)
|
||||
return Gen.ofState(S) == 3 and 999 or Ops.STACK_MAX
|
||||
end
|
||||
|
||||
local function moveId(mon, slot)
|
||||
local move = mon.moves and mon.moves[slot]
|
||||
return type(move) == "table" and (move.moveId or move.id) or move
|
||||
end
|
||||
Ops.ARM_SECONDS = 2.5
|
||||
-- The in-game naming screen caps a nickname at 10 glyphs
|
||||
-- (BattleState:askNicknameUI / src/ui/NamingScreen.lua maxLen = 10); the
|
||||
@@ -427,9 +437,9 @@ end
|
||||
-- the target is the box, not a mon.
|
||||
function Ops.openBoxAddPicker(S, Kit)
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
if #box >= Ops.boxCapacity(S) then
|
||||
if Ops.boxSize(S, box) >= Ops.boxCapacity(S) then
|
||||
return Ops.say(S, ("Box %d is full (%d/%d)")
|
||||
:format(S.selectedBox, #box, Ops.boxCapacity(S)))
|
||||
:format(S.selectedBox, Ops.boxSize(S, box), Ops.boxCapacity(S)))
|
||||
end
|
||||
S.speciesPicker = { query = "", offset = 0, opened = true, mode = "box-add" }
|
||||
if Kit then Kit.focus = "species-picker" end -- soft keyboard rises (#529)
|
||||
@@ -441,20 +451,19 @@ end
|
||||
-- box mon and a party mon born in the editor are indistinguishable.
|
||||
function Ops.boxAddSpecies(S, id)
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
if #box >= Ops.boxCapacity(S) then
|
||||
if Ops.boxSize(S, box) >= Ops.boxCapacity(S) then
|
||||
return Ops.say(S, ("Box %d is full (%d/%d)")
|
||||
:format(S.selectedBox, #box, Ops.boxCapacity(S)))
|
||||
:format(S.selectedBox, Ops.boxSize(S, box), Ops.boxCapacity(S)))
|
||||
end
|
||||
if not Ops.speciesUsable(S, id) then
|
||||
return Ops.say(S, ("%s has no usable base stats, cannot add it")
|
||||
:format(tostring(id)))
|
||||
end
|
||||
local mon = createMon(S, id, 5)
|
||||
table.insert(box, mon)
|
||||
S.selectedBoxSlot = #box
|
||||
S.selectedBoxSlot = boxInsert(S, box, mon)
|
||||
S.editingMon = mon
|
||||
return Ops.mark(S, ("Added %s Lv5 to box %d slot %d")
|
||||
:format(id, S.selectedBox, #box))
|
||||
:format(id, S.selectedBox, S.selectedBoxSlot))
|
||||
end
|
||||
|
||||
function Ops.setDv(S, mon, key, value)
|
||||
@@ -472,7 +481,7 @@ end
|
||||
function Ops.cycleMove(S, mon, slot)
|
||||
if not mon or not (S.cat and S.cat.moves and #S.cat.moves > 0) then return false end
|
||||
local moves = S.cat.moves
|
||||
local current = mon.moves and mon.moves[slot] and mon.moves[slot].id
|
||||
local current = moveId(mon, slot)
|
||||
local idx = 0
|
||||
if current then
|
||||
for i, id in ipairs(moves) do
|
||||
@@ -554,7 +563,7 @@ function Ops.setMove(S, mon, slot, id)
|
||||
if not mon then return false end
|
||||
slot = math.floor(tonumber(slot) or 0)
|
||||
if slot < 1 or slot > 4 then return false end
|
||||
local current = mon.moves and mon.moves[slot] and mon.moves[slot].id
|
||||
local current = moveId(mon, slot)
|
||||
if id == current then
|
||||
return Ops.say(S, ("Move %d is already %s"):format(slot, tostring(id)))
|
||||
end
|
||||
@@ -596,8 +605,8 @@ function Ops.clearMove(S, mon, slot)
|
||||
if not (mon and mon.moves and mon.moves[slot]) then
|
||||
return Ops.say(S, ("Move slot %d is already empty"):format(slot))
|
||||
end
|
||||
local id = mon.moves[slot].id
|
||||
mon.moves[slot] = nil
|
||||
local id = moveId(mon, slot)
|
||||
if Gen.ofState(S) == 3 then MonOps.clearMove(mon, slot) else mon.moves[slot] = nil end
|
||||
return Ops.mark(S, ("Cleared move slot %d (%s)"):format(slot, id))
|
||||
end
|
||||
|
||||
@@ -606,7 +615,10 @@ function Ops.resetMoves(S, mon)
|
||||
local def = S.data.pokemon[mon.species]
|
||||
local gen = Gen.ofState(S)
|
||||
local learned
|
||||
if gen == 2 then
|
||||
if gen == 3 then
|
||||
learned = require("src.core.game3.pokemon").movesAtLevel(mon, mon.level)
|
||||
for slot = 1, 4 do MonOps.clearMove(mon, slot) end
|
||||
elseif gen == 2 then
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
learned = {}
|
||||
for _, mv in ipairs(Mon.movesAtLevel(def, mon.level, S.data.moves)) do
|
||||
@@ -631,9 +643,16 @@ function Ops.healMon(S, mon)
|
||||
mon.hp = mon.stats.hp
|
||||
if mon.maxHp then mon.maxHp = mon.stats.hp end
|
||||
mon.status = nil
|
||||
for _, mv in ipairs(mon.moves or {}) do
|
||||
local def = S.data.moves[mv.id]
|
||||
if def then mv.pp = def.pp + ((mv.ppUps or 0) * math.floor(def.pp / 5)) end
|
||||
for slot, mv in pairs(mon.moves or {}) do
|
||||
if Gen.ofState(S) == 3 then
|
||||
local pp = mon.maxPp and mon.maxPp[slot] or require("src.core.game3.pokemon").movePp(moveId(mon, slot))
|
||||
mon.pp = mon.pp or {}
|
||||
mon.pp[slot] = pp
|
||||
if type(mv) == "table" then mv.pp = pp end
|
||||
else
|
||||
local def = S.data.moves[mv.id]
|
||||
if def then mv.pp = def.pp + ((mv.ppUps or 0) * math.floor(def.pp / 5)) end
|
||||
end
|
||||
end
|
||||
return Ops.mark(S, ("Healed %s to %d/%d HP"):format(mon.species, mon.hp, mon.stats.hp))
|
||||
end
|
||||
@@ -787,6 +806,24 @@ function Ops.clearNickname(S, mon)
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------ boxes
|
||||
function Ops.boxSize(S, box)
|
||||
if Gen.ofState(S) ~= 3 then return #box end
|
||||
local count = 0
|
||||
for slot = 1, 30 do if box[slot] then count = count + 1 end end
|
||||
return count
|
||||
end
|
||||
|
||||
boxInsert = function(S, box, mon)
|
||||
if Gen.ofState(S) ~= 3 then table.insert(box, mon); return #box end
|
||||
local wanted = S.selectedBoxSlot
|
||||
if wanted and wanted >= 1 and wanted <= 30 and not box[wanted] then box[wanted] = mon; return wanted end
|
||||
for slot = 1, 30 do if not box[slot] then box[slot] = mon; return slot end end
|
||||
end
|
||||
|
||||
boxRemove = function(S, box, slot)
|
||||
if Gen.ofState(S) == 3 then box[slot] = nil else table.remove(box, slot) end
|
||||
end
|
||||
|
||||
function Ops.boxCount(S)
|
||||
return Gen.boxCount(S.save)
|
||||
end
|
||||
@@ -803,8 +840,9 @@ function Ops.selectBox(S, index)
|
||||
S.selectedBox = clamp(index, 1, Ops.boxCount(S))
|
||||
S.selectedBoxSlot = 1
|
||||
S.save.currentBox = S.selectedBox
|
||||
if Gen.ofState(S) == 3 then S.save.storage.currentBox = S.selectedBox end
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
S.status = ("Box %d (%d/%d)"):format(S.selectedBox, #box, Ops.boxCapacity(S))
|
||||
S.status = ("Box %d (%d/%d)"):format(S.selectedBox, Ops.boxSize(S, box), Ops.boxCapacity(S))
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -830,17 +868,16 @@ end
|
||||
-- chooses what lands in the box instead of always getting catalog entry #1.
|
||||
function Ops.boxAdd(S)
|
||||
local box = Ops.boxes(S)[S.selectedBox]
|
||||
if #box >= Ops.boxCapacity(S) then
|
||||
if Ops.boxSize(S, box) >= Ops.boxCapacity(S) then
|
||||
return Ops.say(S, ("Box %d is full (%d/%d)")
|
||||
:format(S.selectedBox, #box, Ops.boxCapacity(S)))
|
||||
:format(S.selectedBox, Ops.boxSize(S, box), Ops.boxCapacity(S)))
|
||||
end
|
||||
local species = S.cat.species[1]
|
||||
local mon = createMon(S, species, 5)
|
||||
table.insert(box, mon)
|
||||
S.selectedBoxSlot = #box
|
||||
S.selectedBoxSlot = boxInsert(S, box, mon)
|
||||
S.editingMon = mon
|
||||
return Ops.mark(S, ("Added %s Lv5 to box %d slot %d")
|
||||
:format(species, S.selectedBox, #box))
|
||||
:format(species, S.selectedBox, S.selectedBoxSlot))
|
||||
end
|
||||
|
||||
function Ops.withdraw(S)
|
||||
@@ -857,7 +894,7 @@ function Ops.withdraw(S)
|
||||
if not ok then return Ops.say(S, reason) end
|
||||
Boxes2.withdraw(S.save, S.selectedBox, S.selectedBoxSlot)
|
||||
else
|
||||
table.remove(box, S.selectedBoxSlot)
|
||||
boxRemove(S, box, S.selectedBoxSlot)
|
||||
table.insert(S.save.party, mon)
|
||||
end
|
||||
S.selectedBoxSlot = clamp(S.selectedBoxSlot, 1, math.max(#Ops.boxes(S)[S.selectedBox], 1))
|
||||
@@ -873,7 +910,7 @@ function Ops.release(S)
|
||||
("Release %s permanently? Click again to confirm"):format(mon.species)) then
|
||||
return false
|
||||
end
|
||||
table.remove(box, S.selectedBoxSlot)
|
||||
boxRemove(S, box, S.selectedBoxSlot)
|
||||
if S.editingMon == mon then S.editingMon = nil end
|
||||
S.selectedBoxSlot = clamp(S.selectedBoxSlot, 1, math.max(#box, 1))
|
||||
return Ops.mark(S, ("Released %s"):format(mon.species))
|
||||
@@ -896,6 +933,22 @@ function Ops.deposit(S)
|
||||
if S.editingMon == mon then S.editingMon = nil end
|
||||
return Ops.mark(S, ("Deposited %s into box %d"):format(mon.species, boxIndex))
|
||||
end
|
||||
if Gen.ofState(S) == 3 then
|
||||
local boxes = Ops.boxes(S)
|
||||
local first = S.selectedBox or S.save.currentBox or 1
|
||||
for offset = 0, 13 do
|
||||
local b = ((first - 1 + offset) % 14) + 1
|
||||
if Ops.boxSize(S, boxes[b]) < 30 then
|
||||
local slot = boxInsert(S, boxes[b], mon)
|
||||
table.remove(S.save.party, i)
|
||||
S.selectedParty = clamp(i, 1, math.max(#S.save.party, 1))
|
||||
S.selectedBox, S.selectedBoxSlot = b, slot
|
||||
if S.editingMon == mon then S.editingMon = nil end
|
||||
return Ops.mark(S, ("Deposited %s into box %d"):format(mon.species, b))
|
||||
end
|
||||
end
|
||||
return Ops.say(S, "Every box is full")
|
||||
end
|
||||
local boxNum = BoxesMod.deposit(S.save, mon)
|
||||
if not boxNum then
|
||||
return Ops.say(S, "Every box is full, release something first")
|
||||
@@ -942,19 +995,6 @@ function Ops.maxCoins(S)
|
||||
return Ops.addCoins(S, Ops.COIN_MAX)
|
||||
end
|
||||
|
||||
local function syncG3Bag(S)
|
||||
if Gen.ofState(S) ~= 3 or not S.save then return end
|
||||
local okB, BagG3 = pcall(require, "src.core.game3.bag")
|
||||
if okB and BagG3 then
|
||||
S.save.bag = BagG3.new()
|
||||
for id, qty in pairs(S.save.inventory or {}) do
|
||||
if qty and qty > 0 then
|
||||
BagG3.add(S.save.bag, id, qty)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function itemQty(inv, id)
|
||||
if not inv or id == nil then return 0 end
|
||||
local val = inv[id]
|
||||
@@ -967,42 +1007,30 @@ local function itemQty(inv, id)
|
||||
end
|
||||
Ops.itemQty = itemQty
|
||||
|
||||
local function syncG3Pc(S)
|
||||
if Gen.ofState(S) ~= 3 or not S.save then return end
|
||||
if type(S.save.storage) ~= "table" then
|
||||
S.save.storage = { currentBox = 1, boxes = {}, items = {} }
|
||||
local function changeG3(S, pc, id, quantity)
|
||||
if not id then return Ops.say(S, "Pick an item first") end
|
||||
if not G3.change(S.data, S.save, pc, { { id = id, qty = quantity } }) then
|
||||
return Ops.say(S, "Item change refused: quantity or storage capacity")
|
||||
end
|
||||
if type(S.save.pc) == "table" then
|
||||
S.save.pc.items = nil
|
||||
if next(S.save.pc) == nil then S.save.pc = nil end
|
||||
end
|
||||
local okD, ItemsData = pcall(require, "src.core.game3.items_data")
|
||||
local items = {}
|
||||
for id, val in pairs(S.save.pcItems or {}) do
|
||||
local qty = itemQty(S.save.pcItems, id)
|
||||
if qty and qty > 0 then
|
||||
local num = okD and ItemsData and ItemsData.toNumericId(id) or id
|
||||
items[#items + 1] = { id = num, qty = qty }
|
||||
return Ops.mark(S, ("%s x%d%s"):format(tostring(id), quantity, pc and " in PC storage" or ""))
|
||||
end
|
||||
|
||||
local function maxG3(S, pc)
|
||||
local changes = {}
|
||||
for id, qty in pairs(pc and S.save.pcItems or S.save.inventory) do
|
||||
if type(qty) == "number" and qty > 0 and qty < 999 and Ops.itemStacks(S, id) then
|
||||
changes[#changes + 1] = { id = id, qty = 999 }
|
||||
end
|
||||
end
|
||||
S.save.storage.items = items
|
||||
if #changes == 0 then return Ops.say(S, "Every stack is already maxed") end
|
||||
if not G3.change(S.data, S.save, pc, changes) then return Ops.say(S, "Item changes refused") end
|
||||
return Ops.mark(S, ("Maxed %d stacks to x999"):format(#changes))
|
||||
end
|
||||
|
||||
function Ops.addToBag(S, id)
|
||||
if not id then return Ops.say(S, "Pick an item first") end
|
||||
S.save.inventory = S.save.inventory or {}
|
||||
if Gen.ofState(S) == 3 then
|
||||
local BagG3 = require("src.core.game3.bag")
|
||||
S.save.bag = S.save.bag or BagG3.new()
|
||||
local ok = BagG3.add(S.save.bag, id, 1)
|
||||
if ok then
|
||||
S.save.inventory[id] = itemQty(S.save.inventory, id) + 1
|
||||
Bag.order(S.save, S.data)
|
||||
return Ops.mark(S, ("Added %s to the bag"):format(tostring(id)))
|
||||
else
|
||||
return Ops.say(S, ("Could not add %s to bag (pocket full)"):format(tostring(id)))
|
||||
end
|
||||
end
|
||||
if Gen.ofState(S) == 3 then return changeG3(S, false, id, G3.quantity(S.data, S.save, false, id) + 1) end
|
||||
local pocket = Bag.pocketOf(id, S.data)
|
||||
local capacity = Bag.capacity(S.data, pocket)
|
||||
if Bag.add(S.save, id, 1, S.data) then
|
||||
@@ -1017,31 +1045,10 @@ function Ops.bagAdjust(S, id, delta)
|
||||
if not id then return Ops.say(S, "No bag row selected") end
|
||||
S.save.inventory = S.save.inventory or {}
|
||||
local have = itemQty(S.save.inventory, id)
|
||||
if Gen.ofState(S) == 3 then
|
||||
if delta > 0 then
|
||||
if have >= Ops.STACK_MAX then
|
||||
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.STACK_MAX))
|
||||
end
|
||||
local nextQty = have + delta
|
||||
S.save.inventory[id] = nextQty
|
||||
syncG3Bag(S)
|
||||
else
|
||||
local nextQty = have + delta
|
||||
if nextQty <= 0 then
|
||||
S.save.inventory[id] = nil
|
||||
Bag.order(S.save, S.data)
|
||||
syncG3Bag(S)
|
||||
return Ops.mark(S, ("Removed the last %s from the bag"):format(tostring(id)))
|
||||
else
|
||||
S.save.inventory[id] = nextQty
|
||||
syncG3Bag(S)
|
||||
end
|
||||
end
|
||||
return Ops.mark(S, ("%s x%d"):format(tostring(id), itemQty(S.save.inventory, id)))
|
||||
end
|
||||
if Gen.ofState(S) == 3 then return changeG3(S, false, id, math.max(0, G3.quantity(S.data, S.save, false, id) + delta)) end
|
||||
if delta > 0 then
|
||||
if have >= Ops.STACK_MAX then
|
||||
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.STACK_MAX))
|
||||
if have >= Ops.stackMax(S) then
|
||||
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.stackMax(S)))
|
||||
end
|
||||
Bag.add(S.save, id, delta, S.data)
|
||||
else
|
||||
@@ -1057,12 +1064,7 @@ function Ops.bagDrop(S, id)
|
||||
if not id then return Ops.say(S, "No bag row selected") end
|
||||
S.save.inventory = S.save.inventory or {}
|
||||
local qty = itemQty(S.save.inventory, id)
|
||||
if Gen.ofState(S) == 3 then
|
||||
S.save.inventory[id] = nil
|
||||
Bag.order(S.save, S.data)
|
||||
syncG3Bag(S)
|
||||
return Ops.mark(S, ("Dropped all %d %s"):format(qty, tostring(id)))
|
||||
end
|
||||
if Gen.ofState(S) == 3 then return changeG3(S, false, id, 0) end
|
||||
Bag.remove(S.save, id, qty)
|
||||
return Ops.mark(S, ("Dropped all %d %s"):format(qty, tostring(id)))
|
||||
end
|
||||
@@ -1073,8 +1075,9 @@ end
|
||||
function Ops.itemStacks(S, id)
|
||||
if not id then return false end
|
||||
if Gen.ofState(S) == 3 then
|
||||
local def = S.data and S.data.items and S.data.items[id]
|
||||
return (def and def.pocket) ~= "KEY_ITEMS" and not tostring(id):find("^HM_")
|
||||
local id3 = G3.itemId(S.data, id)
|
||||
local info = require("src.core.game3.items_data")
|
||||
return info.pocketOf(id3) ~= "KEY_ITEMS" and not info.isHm(id3)
|
||||
end
|
||||
if Gen.ofState(S) == 2 then
|
||||
return Bag.pocketOf(id, S.data) ~= "KEY_ITEM"
|
||||
@@ -1086,6 +1089,10 @@ end
|
||||
|
||||
-- engine/items/inventory.asm:74 caps a slot at 99
|
||||
function Ops.bagMax(S, id)
|
||||
if Gen.ofState(S) == 3 then
|
||||
if not id or not Ops.itemStacks(S, id) or G3.quantity(S.data, S.save, false, id) <= 0 then return Ops.say(S, "No stack to max") end
|
||||
return changeG3(S, false, id, 999)
|
||||
end
|
||||
if not id then return Ops.say(S, "No bag row selected") end
|
||||
S.save.inventory = S.save.inventory or {}
|
||||
local have = itemQty(S.save.inventory, id)
|
||||
@@ -1093,22 +1100,17 @@ function Ops.bagMax(S, id)
|
||||
if not Ops.itemStacks(S, id) then
|
||||
return Ops.say(S, ("%s has no quantity to max"):format(tostring(id)))
|
||||
end
|
||||
if have >= Ops.STACK_MAX then
|
||||
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.STACK_MAX))
|
||||
if have >= Ops.stackMax(S) then
|
||||
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.stackMax(S)))
|
||||
end
|
||||
if Gen.ofState(S) == 3 then
|
||||
S.save.inventory[id] = Ops.STACK_MAX
|
||||
syncG3Bag(S)
|
||||
else
|
||||
Bag.add(S.save, id, Ops.STACK_MAX - have, S.data)
|
||||
end
|
||||
return Ops.mark(S, ("%s x%d"):format(tostring(id), Ops.STACK_MAX))
|
||||
Bag.add(S.save, id, Ops.stackMax(S) - have, S.data)
|
||||
return Ops.mark(S, ("%s x%d"):format(tostring(id), Ops.stackMax(S)))
|
||||
end
|
||||
|
||||
function Ops.bagCanMax(S, id)
|
||||
if id ~= nil then
|
||||
local have = itemQty(S.save.inventory, id)
|
||||
return have > 0 and have < Ops.STACK_MAX and Ops.itemStacks(S, id)
|
||||
local have = Gen.ofState(S) == 3 and G3.quantity(S.data, S.save, false, id) or itemQty(S.save.inventory, id)
|
||||
return have > 0 and have < Ops.stackMax(S) and Ops.itemStacks(S, id)
|
||||
end
|
||||
for _, rowId in ipairs(Bag.order(S.save, S.data)) do
|
||||
if Ops.bagCanMax(S, rowId) then return true end
|
||||
@@ -1117,27 +1119,23 @@ function Ops.bagCanMax(S, id)
|
||||
end
|
||||
|
||||
function Ops.bagMaxAll(S)
|
||||
if Gen.ofState(S) == 3 then return maxG3(S, false) end
|
||||
local order = Bag.order(S.save, S.data)
|
||||
local ids = {}
|
||||
for i = 1, #order do ids[i] = order[i] end
|
||||
local n = 0
|
||||
for _, id in ipairs(ids) do
|
||||
local have = itemQty(S.save.inventory, id)
|
||||
if have > 0 and have < Ops.STACK_MAX and Ops.itemStacks(S, id) then
|
||||
if Gen.ofState(S) == 3 then
|
||||
S.save.inventory[id] = Ops.STACK_MAX
|
||||
else
|
||||
Bag.add(S.save, id, Ops.STACK_MAX - have, S.data)
|
||||
end
|
||||
if have > 0 and have < Ops.stackMax(S) and Ops.itemStacks(S, id) then
|
||||
Bag.add(S.save, id, Ops.stackMax(S) - have, S.data)
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
if n == 0 then
|
||||
return Ops.say(S, ("Every bag stack is already at x%d"):format(Ops.STACK_MAX))
|
||||
return Ops.say(S, ("Every bag stack is already at x%d"):format(Ops.stackMax(S)))
|
||||
end
|
||||
if Gen.ofState(S) == 3 then syncG3Bag(S) end
|
||||
return Ops.mark(S, ("Maxed %d bag stack%s to x%d")
|
||||
:format(n, n == 1 and "" or "s", Ops.STACK_MAX))
|
||||
:format(n, n == 1 and "" or "s", Ops.stackMax(S)))
|
||||
end
|
||||
|
||||
-- constants/item_data_constants.asm:41
|
||||
@@ -1220,6 +1218,7 @@ function Ops.pcSort(S, mode)
|
||||
end
|
||||
|
||||
function Ops.addToPc(S, id)
|
||||
if Gen.ofState(S) == 3 then return changeG3(S, true, id, G3.quantity(S.data, S.save, true, id) + 1) end
|
||||
if not id then return Ops.say(S, "Pick an item first") end
|
||||
local pc = Ops.pcItems(S)
|
||||
local n = 0
|
||||
@@ -1228,40 +1227,42 @@ function Ops.addToPc(S, id)
|
||||
return Ops.say(S, "PC item storage is full (50 stacks)")
|
||||
end
|
||||
local cur = itemQty(pc, id)
|
||||
pc[id] = math.min(Ops.STACK_MAX, cur + 1)
|
||||
syncG3Pc(S)
|
||||
pc[id] = math.min(Ops.stackMax(S), cur + 1)
|
||||
return Ops.mark(S, ("%s x%d in PC storage"):format(tostring(id), pc[id]))
|
||||
end
|
||||
|
||||
function Ops.pcAdjust(S, id, delta)
|
||||
if Gen.ofState(S) == 3 then return changeG3(S, true, id, math.max(0, G3.quantity(S.data, S.save, true, id) + delta)) end
|
||||
if not id then return Ops.say(S, "No PC row selected") end
|
||||
local pc = Ops.pcItems(S)
|
||||
local cur = itemQty(pc, id)
|
||||
if not pc[id] and cur <= 0 then return Ops.say(S, ("%s is not in PC storage"):format(tostring(id))) end
|
||||
if delta > 0 and cur >= Ops.STACK_MAX then
|
||||
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.STACK_MAX))
|
||||
if delta > 0 and cur >= Ops.stackMax(S) then
|
||||
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.stackMax(S)))
|
||||
end
|
||||
local nextQty = clamp(cur + delta, 0, Ops.STACK_MAX)
|
||||
local nextQty = clamp(cur + delta, 0, Ops.stackMax(S))
|
||||
if nextQty <= 0 then
|
||||
pc[id] = nil
|
||||
syncG3Pc(S)
|
||||
return Ops.mark(S, ("Removed %s from PC storage"):format(tostring(id)))
|
||||
end
|
||||
pc[id] = nextQty
|
||||
syncG3Pc(S)
|
||||
return Ops.mark(S, ("%s x%d in PC storage"):format(tostring(id), pc[id]))
|
||||
end
|
||||
|
||||
function Ops.pcDrop(S, id)
|
||||
if Gen.ofState(S) == 3 then return changeG3(S, true, id, 0) end
|
||||
if not id then return Ops.say(S, "No PC row selected") end
|
||||
local pc = Ops.pcItems(S)
|
||||
local qty = itemQty(pc, id)
|
||||
pc[id] = nil
|
||||
syncG3Pc(S)
|
||||
return Ops.mark(S, ("Dropped all %d %s from PC storage"):format(qty, tostring(id)))
|
||||
end
|
||||
|
||||
function Ops.pcMax(S, id)
|
||||
if Gen.ofState(S) == 3 then
|
||||
if not id or not Ops.itemStacks(S, id) or G3.quantity(S.data, S.save, true, id) <= 0 then return Ops.say(S, "No stack to max") end
|
||||
return changeG3(S, true, id, 999)
|
||||
end
|
||||
if not id then return Ops.say(S, "No PC row selected") end
|
||||
local pc = Ops.pcItems(S)
|
||||
local cur = itemQty(pc, id)
|
||||
@@ -1269,23 +1270,22 @@ function Ops.pcMax(S, id)
|
||||
if not Ops.itemStacks(S, id) then
|
||||
return Ops.say(S, ("%s has no quantity to max"):format(tostring(id)))
|
||||
end
|
||||
if cur >= Ops.STACK_MAX then
|
||||
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.STACK_MAX))
|
||||
if cur >= Ops.stackMax(S) then
|
||||
return Ops.say(S, ("%s is already at x%d"):format(tostring(id), Ops.stackMax(S)))
|
||||
end
|
||||
pc[id] = Ops.STACK_MAX
|
||||
syncG3Pc(S)
|
||||
return Ops.mark(S, ("%s x%d in PC storage"):format(tostring(id), Ops.STACK_MAX))
|
||||
pc[id] = Ops.stackMax(S)
|
||||
return Ops.mark(S, ("%s x%d in PC storage"):format(tostring(id), Ops.stackMax(S)))
|
||||
end
|
||||
|
||||
function Ops.pcCanMax(S, id)
|
||||
local pc = Ops.pcItems(S)
|
||||
if id ~= nil then
|
||||
local have = itemQty(pc, id)
|
||||
return have > 0 and have < Ops.STACK_MAX and Ops.itemStacks(S, id)
|
||||
local have = Gen.ofState(S) == 3 and G3.quantity(S.data, S.save, true, id) or itemQty(pc, id)
|
||||
return have > 0 and have < Ops.stackMax(S) and Ops.itemStacks(S, id)
|
||||
end
|
||||
for rowId, val in pairs(pc) do
|
||||
local qty = itemQty(pc, rowId)
|
||||
if qty > 0 and qty < Ops.STACK_MAX and Ops.itemStacks(S, rowId) then
|
||||
if qty > 0 and qty < Ops.stackMax(S) and Ops.itemStacks(S, rowId) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
@@ -1293,21 +1293,21 @@ function Ops.pcCanMax(S, id)
|
||||
end
|
||||
|
||||
function Ops.pcMaxAll(S)
|
||||
if Gen.ofState(S) == 3 then return maxG3(S, true) end
|
||||
local pc = Ops.pcItems(S)
|
||||
local n = 0
|
||||
for id, val in pairs(pc) do
|
||||
local qty = itemQty(pc, id)
|
||||
if qty > 0 and qty < Ops.STACK_MAX and Ops.itemStacks(S, id) then
|
||||
pc[id] = Ops.STACK_MAX
|
||||
if qty > 0 and qty < Ops.stackMax(S) and Ops.itemStacks(S, id) then
|
||||
pc[id] = Ops.stackMax(S)
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
if n == 0 then
|
||||
return Ops.say(S, ("Every PC stack is already at x%d"):format(Ops.STACK_MAX))
|
||||
return Ops.say(S, ("Every PC stack is already at x%d"):format(Ops.stackMax(S)))
|
||||
end
|
||||
syncG3Pc(S)
|
||||
return Ops.mark(S, ("Maxed %d PC stack%s to x%d")
|
||||
:format(n, n == 1 and "" or "s", Ops.STACK_MAX))
|
||||
:format(n, n == 1 and "" or "s", Ops.stackMax(S)))
|
||||
end
|
||||
|
||||
-- Badges are truthy inventory flags, not stackable items, which is why the
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
local PartyMod = require("src.pokemon.Party")
|
||||
local Theme = require("Theme")
|
||||
local Ops = require("Ops")
|
||||
local Gen = require("Gen")
|
||||
local PAL = Theme.PAL
|
||||
|
||||
local M = {}
|
||||
@@ -43,7 +44,7 @@ local function drawStrip(S, Kit, boxes, x, y, stripW, h)
|
||||
if Kit.row(x + pad, ry, stripInner, bRowH, i == S.selectedBox, PAL.blue, 9 * s) then
|
||||
Ops.selectBox(S, i)
|
||||
end
|
||||
local fill = #boxes[i]
|
||||
local fill = Ops.boxSize(S, boxes[i])
|
||||
Kit.text("mono", ("Box %d"):format(i), x + pad + 10 * s,
|
||||
ry + (bRowH - Kit.textHeight("mono")) / 2, PAL.text)
|
||||
local countW = Kit.textWidth("tiny", tostring(fill))
|
||||
@@ -65,7 +66,7 @@ local function drawGrid(S, Kit, box, gridX, y, gridW, h)
|
||||
Kit.text("tab", ("Box %d"):format(S.selectedBox), gx,
|
||||
y + gpad + (headH - Kit.textHeight("tab")) / 2, PAL.heading)
|
||||
local titleW = Kit.textWidth("tab", ("Box %d"):format(S.selectedBox))
|
||||
Kit.text("mono", ("%d/%d"):format(#box, Ops.boxCapacity(S)),
|
||||
Kit.text("mono", ("%d/%d"):format(Ops.boxSize(S, box), Ops.boxCapacity(S)),
|
||||
gx + titleW + 14 * s, y + gpad + (headH - Kit.textHeight("mono")) / 2, PAL.caption)
|
||||
local navW = 34 * s
|
||||
if Kit.stepper(gx + ginner - 2 * navW - 8 * s, y + gpad, navW, headH, "<",
|
||||
@@ -101,7 +102,7 @@ local function drawGrid(S, Kit, box, gridX, y, gridW, h)
|
||||
end
|
||||
if Kit.button(gx + wdW + 10 * s, actY, addW, actH, addLabel,
|
||||
{ font = "small", radius = 9 * s,
|
||||
enabled = #box < Ops.boxCapacity(S) }) then
|
||||
enabled = Ops.boxSize(S, box) < Ops.boxCapacity(S) }) then
|
||||
Ops.openBoxAddPicker(S, Kit)
|
||||
end
|
||||
if Kit.button(gx + ginner - relW, actY, relW, actH, relLabel,
|
||||
@@ -158,7 +159,7 @@ local function drawGrid(S, Kit, box, gridX, y, gridW, h)
|
||||
Kit.textCenter("micro", "+", bx, by + cellH / 2 - Kit.textHeight("micro") / 2,
|
||||
cellW, PAL.faint)
|
||||
if Kit.press(bx, by, cellW, cellH) then
|
||||
S.selectedBoxSlot = math.min(i, #box + 1)
|
||||
S.selectedBoxSlot = Gen.ofState(S) == 3 and i or math.min(i, Ops.boxSize(S, box) + 1)
|
||||
Ops.openBoxAddPicker(S, Kit)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,7 +41,7 @@ local function quantityRow(S, Kit, x, y, w, h, id, qty, selected, onMinus, onPlu
|
||||
onPlus()
|
||||
end
|
||||
if Kit.stepper(bx + 2 * (btn + 6 * s), y + (h - btn) / 2, btn, btn,
|
||||
tostring(Ops.STACK_MAX), { font = "tiny", enabled = canMax }) then
|
||||
tostring(Ops.stackMax(S)), { font = "tiny", enabled = canMax }) then
|
||||
onMax()
|
||||
end
|
||||
if Kit.button(bx + 3 * (btn + 6 * s), y + (h - btn) / 2, btn, btn, "x",
|
||||
@@ -298,7 +298,7 @@ end
|
||||
local function drawBag(S, Kit, x, y, w, h)
|
||||
S.save.inventory = S.save.inventory or {}
|
||||
local order = Bag.order(S.save, S.data) or {}
|
||||
local capacity = Bag.capacity(S.data) or 20
|
||||
local capacity = Gen.ofState(S) == 3 and 186 or Bag.capacity(S.data) or 20
|
||||
local slots = Bag.slots(S.save, S.data) or 0
|
||||
S.bagOffset = drawQuantityCard(S, Kit, x, y, w, h, {
|
||||
title = "BAG",
|
||||
@@ -330,7 +330,7 @@ local function drawPc(S, Kit, x, y, w, h)
|
||||
counter = ("%d kinds"):format(#pcOrder),
|
||||
order = pcOrder,
|
||||
offset = S.pcOffset or 0,
|
||||
empty = "PC storage is empty. Items sent here have no slot cap.",
|
||||
empty = "PC storage is empty. ",
|
||||
qty = function(id) return (S.save.pcItems and S.save.pcItems[id]) or 0 end,
|
||||
selected = function() return S.selectedPcId end,
|
||||
select = function(id)
|
||||
|
||||
@@ -233,11 +233,11 @@ local function drawMoveRows(S, Kit, mon, rightX, rowY, colW, rowH, rowGap)
|
||||
local mvId = nil
|
||||
local mvPp = 0
|
||||
if type(mv) == "table" then
|
||||
mvId = mv.id or mv.name or (mv.moveId and (S.data and S.data.moves and S.data.moves[mv.moveId] and S.data.moves[mv.moveId].name))
|
||||
mvId = (S.data and S.data.moves and S.data.moves[mv.id] and S.data.moves[mv.id].name) or mv.id or mv.name
|
||||
mvPp = mv.pp or 0
|
||||
elseif type(mv) == "number" then
|
||||
mvId = S.data and S.data.moves and S.data.moves[mv] and S.data.moves[mv].name
|
||||
mvPp = S.data and S.data.moves and S.data.moves[mv] and S.data.moves[mv].pp or 10
|
||||
mvPp = mon.pp and mon.pp[slot] or 0
|
||||
elseif type(mv) == "string" then
|
||||
mvId = mv
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user