mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-29 07:00:55 -04:00
Merge remote-tracking branch 'upstream/dev' into nexhas28/gen3-review-v3-fixes
# Conflicts: # src/core/game3/battle/anim_seq.lua # src/core/game3/battle/effects/secondary.lua # src/core/game3/battle/engine.lua # src/core/game3/battle/state.lua # src/core/game3/battle/switch_seq.lua # tests/engine/game3_knock_off_item_test.lua
This commit is contained in:
@@ -825,15 +825,34 @@ jobs:
|
||||
# Everyone whose commits are in the range: GitHub login when the
|
||||
# commit is linked to an account, the raw git author name when not;
|
||||
# CI bots filtered out.
|
||||
base="${prev_tag:-$(git rev-list --max-parents=0 "$GITHUB_SHA" | tail -1)}"
|
||||
if [ -n "$prev_tag" ]; then
|
||||
commits_endpoint="repos/$GITHUB_REPOSITORY/compare/${prev_tag}...${GITHUB_SHA}?per_page=100"
|
||||
else
|
||||
# No previous release: include all reachable commits, even the root.
|
||||
commits_endpoint="repos/$GITHUB_REPOSITORY/commits?sha=${GITHUB_SHA}&per_page=100"
|
||||
fi
|
||||
contributors="$(gh api --paginate \
|
||||
"repos/$GITHUB_REPOSITORY/compare/${base}...${GITHUB_SHA}" \
|
||||
--jq '.commits[] | if .author and .author.login
|
||||
"$commits_endpoint" \
|
||||
--jq '(if type == "array" then . else .commits end)[]
|
||||
| if .author and .author.login
|
||||
then "@" + .author.login
|
||||
else .commit.author.name end' 2>/dev/null \
|
||||
| grep -viE '\[bot\]$' | sort -uf | sed 's/^/- /' || true)"
|
||||
|
||||
# Use the dev -> main promotion PR for this release commit, not an
|
||||
# individual feature PR whose commits also happen to be in the range.
|
||||
promotion_body="$(gh api --paginate --slurp \
|
||||
"repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls?per_page=100" 2>/dev/null \
|
||||
| jq -r --arg repo "$GITHUB_REPOSITORY" --arg sha "$GITHUB_SHA" '
|
||||
[.[][] | select(.merged_at != null
|
||||
and .base.repo.full_name == $repo and .base.ref == "main"
|
||||
and .head.repo.full_name == $repo and .head.ref == "dev"
|
||||
and .merge_commit_sha == $sha)]
|
||||
| sort_by(.merged_at) | last | .body // ""' || true)"
|
||||
notes="Download the right version for your device below."
|
||||
if [ -n "$(printf '%s' "$promotion_body" | tr -d '[:space:]')" ]; then
|
||||
notes="$promotion_body"
|
||||
fi
|
||||
if [ -n "$closed" ]; then
|
||||
notes+=$'\n\n## Issues closed\n\n'"$closed"
|
||||
fi
|
||||
|
||||
@@ -2703,8 +2703,7 @@ function Battle.update(dt, game)
|
||||
|
||||
local Naming = package.loaded["src.ui.game3.naming"]
|
||||
if Naming and Naming.isOpen and Naming.isOpen() then
|
||||
if input then Naming.handleInput(input) end
|
||||
Naming.update(dt or (1 / 60))
|
||||
-- Runtime ticks Hud after Battle; the naming stack entry owns this input.
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
@@ -228,6 +228,7 @@ function Runtime.update(dt)
|
||||
if not Runtime.active then return end
|
||||
local game = Runtime._game
|
||||
local Hud = require("src.ui.game3.hud")
|
||||
local inputTop = require("src.ui.game3.stack").top() or false
|
||||
local inMenu = Hud.isMenuOpen and Hud.isMenuOpen() or false
|
||||
Runtime.drainDeferred()
|
||||
Runtime.noteFieldFocus(Runtime.fieldScreenOpen(inMenu))
|
||||
@@ -270,21 +271,15 @@ function Runtime.update(dt)
|
||||
end
|
||||
local Message = package.loaded["src.ui.game3.message"]
|
||||
if Message and Message.tick then Message.tick() end
|
||||
Hud.update(game, dt)
|
||||
Hud.update(game, dt, inputTop)
|
||||
return
|
||||
end
|
||||
|
||||
local okN, Naming = pcall(require, "src.ui.game3.naming")
|
||||
if okN and Naming.isOpen and Naming.isOpen() then
|
||||
Naming.handleInput(game and game.input)
|
||||
Naming.update(dt)
|
||||
end
|
||||
|
||||
if not inMenu then
|
||||
local Field = require("src.core.game3.field")
|
||||
Field.update(dt)
|
||||
end
|
||||
Hud.update(game, dt)
|
||||
Hud.update(game, dt, inputTop)
|
||||
end
|
||||
|
||||
function Runtime.uiBusy()
|
||||
|
||||
@@ -78,13 +78,11 @@ end
|
||||
|
||||
-- Script local scratch space: pret's ScriptContext.data[4] (include/script.h:21).
|
||||
local function local_get(ctx, i)
|
||||
ctx.locals = ctx.locals or {}
|
||||
return tonumber(ctx.locals[(tonumber(i) or 0) + 1]) or 0
|
||||
return ctx.data[tonumber(i) or 0] or 0
|
||||
end
|
||||
|
||||
local function local_set(ctx, i, v)
|
||||
ctx.locals = ctx.locals or {}
|
||||
ctx.locals[(tonumber(i) or 0) + 1] = tonumber(v) or 0
|
||||
ctx.data[tonumber(i) or 0] = v or 0
|
||||
end
|
||||
|
||||
-- The port has no flat address space, so the *ptr family shares a synthetic
|
||||
@@ -97,12 +95,13 @@ end
|
||||
|
||||
local function mem_set(ctx, ptr, v)
|
||||
ctx.scriptMem = ctx.scriptMem or {}
|
||||
ctx.scriptMem[tonumber(ptr) or 0] = tonumber(v) or 0
|
||||
ctx.scriptMem[tonumber(ptr) or 0] = (tonumber(v) or 0) % 256
|
||||
end
|
||||
|
||||
-- pret src/scrcmd.c:358 Compare()
|
||||
local function cmp(a, b)
|
||||
a, b = tonumber(a) or 0, tonumber(b) or 0
|
||||
-- pokefirered/src/scrcmd.c:368: local comparisons read the low byte.
|
||||
a, b = (tonumber(a) or 0) % 256, (tonumber(b) or 0) % 256
|
||||
if a < b then return 0 end
|
||||
if a == b then return 1 end
|
||||
return 2
|
||||
|
||||
+10
-1
@@ -174,11 +174,17 @@ function Hud.sampleFieldInput(game)
|
||||
}
|
||||
end
|
||||
|
||||
function Hud.update(game, _dt)
|
||||
function Hud.update(game, _dt, inputTop)
|
||||
local dt = tonumber(_dt) or (1 / 60)
|
||||
|
||||
-- Active stack modal menu tick
|
||||
local top = Stack.top()
|
||||
local namingTick = top and top.id == "naming"
|
||||
if namingTick and top.mod and top.mod.handleInput then
|
||||
-- Naming consumes input before its page-swap timer can unlock the keyboard.
|
||||
-- A prompt that opened it during this frame keeps its opening button press.
|
||||
if inputTop == nil or top == inputTop then top.mod.handleInput(game and game.input) end
|
||||
end
|
||||
if top and top.mod and top.mod.update then
|
||||
pcall(top.mod.update, dt)
|
||||
end
|
||||
@@ -213,6 +219,9 @@ function Hud.update(game, _dt)
|
||||
end
|
||||
end
|
||||
|
||||
-- Do not replay naming input or leak its closing press to the menu underneath.
|
||||
if namingTick then return end
|
||||
|
||||
-- Active stack modal menu input takes top precedence.
|
||||
-- When battle is active, overlays like EvolutionScene or modal stack menus still receive input.
|
||||
if Stack.busy() then
|
||||
|
||||
@@ -121,6 +121,7 @@ return function(game)
|
||||
end
|
||||
U.shot(game, DIR .. "/stitchuif_naming_pc_01_keyboard.png")
|
||||
typedName = (Naming._state and Naming._state.name) or ""
|
||||
result(typedName == "AAA", "three A taps type exactly AAA, got " .. typedName)
|
||||
U.tap(game, "start")
|
||||
U.wait(20)
|
||||
U.tap(game, "a")
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
-- and it is not what stops the item returning on switch-out; the send-out mask
|
||||
-- below is.
|
||||
--
|
||||
-- persist_item resolves the party mon via State.partyMon(b) (= b._partyMon or
|
||||
-- b.mon), so this drives the real effect with plain battler tables.
|
||||
-- luajit tests/engine/game3_knock_off_item_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
@@ -24,10 +22,15 @@ local check, eq = T.check, T.eq
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local Secondary = require("src.core.game3.battle.effects.secondary")
|
||||
local State = require("src.core.game3.battle.state")
|
||||
|
||||
local function adapter()
|
||||
local State = require("src.core.game3.battle.state")
|
||||
local Engine = require("src.core.game3.battle.engine")
|
||||
local SwitchSeq = require("src.core.game3.battle.switch_seq")
|
||||
|
||||
local function adapter(st)
|
||||
return {
|
||||
_st = st,
|
||||
pushEvent = function() end,
|
||||
abilityOf = function(_, b) return b.ability end,
|
||||
hp = function(_, b) return b.hp or 100 end,
|
||||
ownSide = function() return nil end,
|
||||
@@ -49,9 +52,9 @@ local function battler(side, item, ability)
|
||||
}
|
||||
end
|
||||
|
||||
local function knock_off(user, target)
|
||||
local function knock_off(user, target, st)
|
||||
return Secondary.set({
|
||||
adapter = adapter(), user = user, target = target,
|
||||
adapter = adapter(st), user = user, target = target,
|
||||
move = { moveName = "KNOCK OFF" },
|
||||
}, "KNOCK_OFF", false, true, false)
|
||||
end
|
||||
@@ -93,4 +96,38 @@ eq(mon.item, 13, "the party mon still holds the item after the send-out")
|
||||
local control = State.makeBattler(mon, "enemy", { partyIndex = 1, st = {} })
|
||||
eq(control.item, 13, "without the mark the send-out reads the item back")
|
||||
|
||||
-- 6. The mark is per party slot and survives every switch path the engine
|
||||
-- exposes: engine switch-in, presentation send-out, player switch and shift
|
||||
-- switch -- and it does not leak into the next battle.
|
||||
local function fresh_mon(item)
|
||||
return { species = 4, level = 10, hp = 30, maxHp = 30,
|
||||
item = item, heldItem = item, ability = "NONE", moves = { 10 }, pp = { 35 } }
|
||||
end
|
||||
for _, side in ipairs({ "player", "enemy" }) do
|
||||
local party, foes = { fresh_mon(13), fresh_mon(14) }, { fresh_mon(13), fresh_mon(14) }
|
||||
local bst = State.new({ playerParty = party, foeParty = foes })
|
||||
local victim = bst[side]
|
||||
local foe = side == "player" and bst.enemy or bst.player
|
||||
local members = side == "player" and party or foes
|
||||
knock_off(foe, victim, bst)
|
||||
check(State.isKnockedOff(bst, victim), "the victim's party slot is marked")
|
||||
check(not State.isKnockedOff(bst, foe), "the opposite side's same slot is unaffected")
|
||||
Engine.performSwitch(bst, adapter(bst), side, 2)
|
||||
eq(bst[side].item, 14, "another party slot keeps its active item")
|
||||
Engine.performSwitch(bst, adapter(bst), side, 1)
|
||||
eq(bst[side].item, 0, "engine switch-in suppresses the knocked-off item")
|
||||
eq(members[1].heldItem, 13, "switching preserves the party item")
|
||||
SwitchSeq.beginSendOut(bst, side, 1, { headless = true })
|
||||
eq(bst[side].item, 0, "presentation send-out also suppresses the item")
|
||||
if side == "player" then
|
||||
SwitchSeq.beginPlayerSwitch(bst, 1, { headless = true })
|
||||
eq(bst.player.item, 0, "player switch sequence suppresses the item")
|
||||
end
|
||||
SwitchSeq.beginShiftSwitch(bst, 1, 1, { headless = true })
|
||||
eq(bst[side].item, 0, "shift switch sequence suppresses the item")
|
||||
local fresh = State.new({ playerParty = party, foeParty = foes })
|
||||
eq(fresh[side].item, 13, "the original held item works in the next battle")
|
||||
check(not State.isKnockedOff(fresh, fresh[side]), "the new battle has no knock-off mark")
|
||||
end
|
||||
|
||||
T.finish("game3_knock_off_item_test")
|
||||
|
||||
@@ -62,4 +62,46 @@ st4.swapT = 0
|
||||
pcall(Naming.handleInput, input({ down = true }))
|
||||
eq(st4.row, 1, "input is ignored while the page swap is running")
|
||||
|
||||
-- The real runtime must leave stack input and timers to Hud in both branches.
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Battle = require("src.core.game3.battle")
|
||||
Runtime.active = true
|
||||
Runtime.session = {}
|
||||
for _, inBattle in ipairs({ false, true }) do
|
||||
Battle._active = inBattle
|
||||
local active = open_naming()
|
||||
Runtime._game = { input = input({ a = true }) }
|
||||
Runtime.update(1 / 60)
|
||||
eq(active.name, "A", "one runtime press adds one character, battle=" .. tostring(inBattle))
|
||||
eq(active.blink, 1 / 60, "runtime advances the naming timer once")
|
||||
active.pcPages = { "first", "second", "third" }
|
||||
active.pcPage = 1
|
||||
Runtime.update(1 / 60)
|
||||
eq(active.pcPage, 2, "one runtime press advances one result page")
|
||||
local swapping = open_naming()
|
||||
swapping.swapT = 124
|
||||
swapping.swapTo = 2
|
||||
Runtime.update(1 / 60)
|
||||
eq(swapping.name, "", "input remains blocked on the swap-completion frame")
|
||||
eq(swapping.swapT, nil, "the page swap finishes")
|
||||
Runtime.update(1 / 60)
|
||||
eq(swapping.name, "a", "the next frame accepts input on the new page")
|
||||
end
|
||||
Battle._active = false
|
||||
Stack.clear()
|
||||
-- A prompt that opens Naming during the battle update must consume its A.
|
||||
Battle._active = true
|
||||
local battleUpdate = Battle.update
|
||||
local opened
|
||||
Battle.update = function() opened = open_naming() end
|
||||
Runtime._game = { input = input({ a = true }) }
|
||||
Runtime.update(1 / 60)
|
||||
eq(opened.name, "", "the opening prompt's A does not type a character")
|
||||
Battle.update = battleUpdate
|
||||
Runtime.update(1 / 60)
|
||||
eq(opened.name, "A", "the following frame's A reaches naming")
|
||||
Battle._active = false
|
||||
Runtime.active = false
|
||||
Stack.clear()
|
||||
|
||||
T.finish("game3_naming_update_contract_test")
|
||||
|
||||
@@ -207,20 +207,22 @@ end
|
||||
|
||||
-- 16. Script locals and the synthetic pointer store (src/scrcmd.c:293-375).
|
||||
vm = new_vm()
|
||||
vm.ctx.locals = { 10, 20 }
|
||||
Ops.dispatch(vm, { op = "loadbyte", 0, 10 })
|
||||
Ops.dispatch(vm, { op = "loadbyte", 1, 20 })
|
||||
Ops.dispatch(vm, { op = "copylocal", [1] = 0, [2] = 1 })
|
||||
eq(vm.ctx.locals[1], 20, "copylocal copies a local")
|
||||
vm.ctx.locals = { 5, 9 }
|
||||
eq(vm.ctx.data[0], 20, "copylocal copies a local")
|
||||
Ops.dispatch(vm, { op = "loadbyte", 0, 5 })
|
||||
Ops.dispatch(vm, { op = "loadbyte", 1, 9 })
|
||||
Ops.dispatch(vm, { op = "compare_local_to_local", [1] = 0, [2] = 1 })
|
||||
eq(vm.ctx.comparisonResult, 0, "compare_local_to_local reports LT")
|
||||
vm.ctx.locals = { 7, 7 }
|
||||
Ops.dispatch(vm, { op = "loadbyte", 0, 7 })
|
||||
Ops.dispatch(vm, { op = "compare_local_to_value", [1] = 0, [2] = 7 })
|
||||
eq(vm.ctx.comparisonResult, 1, "compare_local_to_value reports EQ")
|
||||
Ops.dispatch(vm, { op = "setptr", [1] = 42, [2] = 0x1234 })
|
||||
eq(vm.ctx.scriptMem[0x1234], 42, "setptr writes the synthetic byte store")
|
||||
Ops.dispatch(vm, { op = "loadbytefromptr", [1] = 1, [2] = 0x1234 })
|
||||
eq(vm.ctx.locals[2], 42, "loadbytefromptr reads it back into a local")
|
||||
vm.ctx.locals[3] = 9
|
||||
eq(vm.ctx.data[1], 42, "loadbytefromptr reads it back into a local")
|
||||
Ops.dispatch(vm, { op = "loadbyte", 2, 9 })
|
||||
Ops.dispatch(vm, { op = "setptrbyte", [1] = 2, [2] = 0x1235 })
|
||||
eq(vm.ctx.scriptMem[0x1235], 9, "setptrbyte stores a local")
|
||||
Ops.dispatch(vm, { op = "copybyte", [1] = 0x1236, [2] = 0x1235 })
|
||||
@@ -230,6 +232,18 @@ eq(vm.ctx.comparisonResult, 0, "compare_local_to_ptr compares local vs store")
|
||||
Ops.dispatch(vm, { op = "compare_ptr_to_ptr", [1] = 0x1234, [2] = 0x1236 })
|
||||
eq(vm.ctx.comparisonResult, 2, "compare_ptr_to_ptr compares two stored bytes")
|
||||
|
||||
Ops.dispatch(vm, { op = "loadword", 0, 0x12AB })
|
||||
Ops.dispatch(vm, { op = "copylocal", 3, 0 })
|
||||
eq(vm.ctx.data[3], 0x12AB, "copylocal preserves the full loadword register")
|
||||
Ops.dispatch(vm, { op = "compare_local_to_value", 3, 0xAB })
|
||||
eq(vm.ctx.comparisonResult, 1, "local comparisons read the low byte")
|
||||
Ops.dispatch(vm, { op = "setptrbyte", 3, 0x1237 })
|
||||
Ops.dispatch(vm, { op = "loadbytefromptr", 0, 0x1237 })
|
||||
eq(vm.ctx.data[0], 0xAB, "pointer writes truncate a word register to a byte")
|
||||
Ops.dispatch(vm, { op = "loadword", 0, "std:1" })
|
||||
Ops.dispatch(vm, { op = "copylocal", 1, 0 })
|
||||
eq(vm.ctx.data[1], "std:1", "copylocal preserves the host's symbolic text pointers")
|
||||
|
||||
-- 17. The RAM-script (v*) control flow (src/scrcmd.c:171-209, :1580).
|
||||
vm = new_vm()
|
||||
vm:setPc("t_main", 4)
|
||||
|
||||
Reference in New Issue
Block a user