mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-02 23:55:22 -04:00
further save editor yum
This commit is contained in:
@@ -501,6 +501,41 @@ jobs:
|
||||
headless:
|
||||
name: headless suites (no ROM)
|
||||
runs-on: ubuntu-latest
|
||||
needs: headless-groups
|
||||
if: ${{ always() }}
|
||||
steps:
|
||||
- name: require every headless group to pass
|
||||
env:
|
||||
HEADLESS_RESULT: ${{ needs.headless-groups.result }}
|
||||
run: |
|
||||
if [ "$HEADLESS_RESULT" != "success" ]; then
|
||||
echo "Headless groups finished with $HEADLESS_RESULT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
headless-groups:
|
||||
name: headless / ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- group: engine
|
||||
name: engine
|
||||
- group: gen2
|
||||
name: Gen 2
|
||||
- group: save-codecs
|
||||
name: save codecs
|
||||
- group: save-records
|
||||
name: Gen 3 save records
|
||||
- group: save-world
|
||||
name: Gen 3 save world
|
||||
- group: save-editor
|
||||
name: save editor
|
||||
- group: mods
|
||||
name: mod tools
|
||||
- group: game3
|
||||
name: Gen 3 runtime
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
@@ -516,8 +551,8 @@ jobs:
|
||||
- name: interpreter version
|
||||
run: luajit -v
|
||||
|
||||
- name: run every ROM-free tier
|
||||
run: ./scripts/test.sh
|
||||
- name: run ROM-free group
|
||||
run: ./scripts/test.sh --group "${{ matrix.group }}"
|
||||
|
||||
fixture-dataset:
|
||||
name: fixture dataset integrity
|
||||
|
||||
+59
-4
@@ -10,6 +10,9 @@
|
||||
#
|
||||
# scripts/test.sh every tier this checkout can run
|
||||
# scripts/test.sh --quick skip the slow content tier
|
||||
# scripts/test.sh --group NAME one ROM-free CI group
|
||||
# scripts/test.sh --list show selected tiers without running them
|
||||
# scripts/test.sh --list-groups list the ROM-free CI groups
|
||||
# scripts/test.sh --bless re-pin the fingerprint goldens
|
||||
# WITH_SHOTS=1 scripts/test.sh also capture and diff golden shots
|
||||
# (fails today -- see the T5 block below)
|
||||
@@ -33,17 +36,40 @@ LUA54=${LUA54:-lua5.4}
|
||||
BLESS=0
|
||||
QUICK=0
|
||||
SHOTS=${WITH_SHOTS:-0}
|
||||
GROUP=all
|
||||
LIST=0
|
||||
ACTIVE_GROUP=engine
|
||||
SELECTED_TIERS=0
|
||||
ROM_FREE_GROUPS=(engine gen2 save-codecs save-records save-world save-editor mods game3)
|
||||
|
||||
for arg in "$@"; do
|
||||
while [ $# -gt 0 ]; do
|
||||
arg="$1"
|
||||
case "$arg" in
|
||||
--bless) BLESS=1 ;;
|
||||
--bless-shots) SHOTS=1; BLESS=1 ;;
|
||||
--quick) QUICK=1 ;;
|
||||
--help|-h) sed -n '2,38p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
--group) [ $# -ge 2 ] || { echo "--group needs a name" >&2; exit 2; }; GROUP="$2"; shift ;;
|
||||
--list) LIST=1 ;;
|
||||
--list-groups) printf '%s\n' "${ROM_FREE_GROUPS[@]}"; exit 0 ;;
|
||||
--help|-h) awk 'NR == 1 { next } /^set -uo pipefail/ { exit } { sub(/^# ?/, ""); print }' "$0"; exit 0 ;;
|
||||
*) echo "unknown option: $arg" >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
case "$GROUP" in
|
||||
all|engine|gen2|save-codecs|save-records|save-world|save-editor|mods|game3) ;;
|
||||
*) echo "unknown test group: $GROUP (use --list-groups)" >&2; exit 2 ;;
|
||||
esac
|
||||
if [ "$GROUP" != all ] && { [ "$BLESS" = 1 ] || [ "$SHOTS" = 1 ]; }; then
|
||||
echo "--group selects ROM-free tests; blessing and screenshots require the full suite" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ "$LIST" = 1 ] && { [ "$BLESS" = 1 ] || [ "$SHOTS" = 1 ]; }; then
|
||||
echo "--list cannot bless goldens or capture screenshots" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v "$LUA" >/dev/null 2>&1; then
|
||||
echo "no lua interpreter '$LUA' on PATH (set LUA=...)" >&2
|
||||
exit 2
|
||||
@@ -133,6 +159,13 @@ fi
|
||||
FAILED=()
|
||||
run_tier() {
|
||||
local label="$1"; shift
|
||||
[ "$GROUP" = all ] || [ "$GROUP" = "$ACTIVE_GROUP" ] || return 0
|
||||
SELECTED_TIERS=$((SELECTED_TIERS + 1))
|
||||
if [ "$LIST" = 1 ]; then
|
||||
printf '[tier] %s\n' "$label"
|
||||
printf '[command]'; printf ' %q' "$@"; printf '\n'
|
||||
return 0
|
||||
fi
|
||||
echo ""
|
||||
echo "=============================================================="
|
||||
echo " $label"
|
||||
@@ -175,15 +208,26 @@ run_tier "T0 URI launch arguments" "$LUA" tests/engine/launch_uri_args_test.lua
|
||||
run_tier "T1/T2 engine invariants + parity gates" "$LUA" tests/run_engine.lua
|
||||
# Gen 2 / Crystal: ROM-free (own fixtures, or a self-skip on a missing cache),
|
||||
# so it runs here rather than behind the Red content gate below.
|
||||
ACTIVE_GROUP=gen2
|
||||
run_tier "T2 Gen 2 / Crystal suites" "$LUA" tests/run_gen2.lua
|
||||
run_tier "T2 save compat (R1/R2 round trips, reader validator)" "$LUA" tests/run_save_compat.lua
|
||||
ACTIVE_GROUP=save-codecs
|
||||
run_tier "T2 save compat: codecs + cross-generation contracts" "$LUA" tests/run_save_compat.lua --group codecs
|
||||
ACTIVE_GROUP=save-records
|
||||
run_tier "T2 save compat: Gen 3 records + gifts" "$LUA" tests/run_save_compat.lua --group records
|
||||
ACTIVE_GROUP=save-world
|
||||
run_tier "T2 save compat: Gen 3 towns + TV" "$LUA" tests/run_save_compat.lua --group world
|
||||
# The mobile UI and input suites use committed fixtures, so they must run
|
||||
# even on CI's ROM-free checkout. Keep the native-cache property suite below.
|
||||
ACTIVE_GROUP=save-editor
|
||||
run_tier "T2 save editor: mobile properties (fixtures)" \
|
||||
env POKEPORT_DATA_DIR=tests/fixture_data "$LUA" tests/save_editor_mobile_properties_tests.lua
|
||||
run_tier "T2 save editor: Gen 3 IV / EV / PP (fixtures)" "$LUA" tests/save_editor_gen3_ev_iv_tests.lua
|
||||
run_tier "T2 save editor: touch value controls" \
|
||||
env POKEPORT_DATA_DIR=tests/fixture_data "$LUA" tests/save_editor_touch_controls_test.lua
|
||||
run_tier "T2 save editor: wheel scrolling" "$LUA" tests/save_editor_wheel_bug595_test.lua
|
||||
run_tier "T2 save editor: pad / NX input" "$LUA" tests/save_editor_pad_input_test.lua
|
||||
run_tier "T2 save editor: gold / gen2 (fixtures)" "$LUA" tests/save_editor_gen2_tests.lua
|
||||
ACTIVE_GROUP=mods
|
||||
run_tier "T4 mod-SDK" "$LUA" tests/run_modkit.lua
|
||||
run_tier "T4 modkit dev tooling (fixture)" "$LUA" tests/modkit_tests.lua
|
||||
|
||||
@@ -298,10 +342,12 @@ run_game3_tier() {
|
||||
rm -rf "$tmp"
|
||||
return 0
|
||||
}
|
||||
ACTIVE_GROUP=game3
|
||||
run_tier "T6 game3 top-level scenario suites" run_game3_tier
|
||||
|
||||
# This suite loads the imported FireRed tables itself and self-skips when
|
||||
# absent. It must not depend on also having a Red dataset installed.
|
||||
ACTIVE_GROUP=save-editor
|
||||
if [ -n "$GAME3_IDENTITY" ]; then
|
||||
run_tier "T6 save editor: native property legality" \
|
||||
env POKEPORT_IDENTITY="$GAME3_IDENTITY" "$LUA" tests/save_editor_property_legality_test.lua
|
||||
@@ -311,6 +357,9 @@ elif [ -n "$GAME3_GBA_CACHE" ]; then
|
||||
else
|
||||
run_tier "T6 save editor: native property legality" "$LUA" tests/save_editor_property_legality_test.lua
|
||||
fi
|
||||
run_tier "T6 save editor: native bulk actions" \
|
||||
env POKEPORT_TOUCH_NATIVE=1 POKEPORT_IDENTITY="${POKEPORT_IDENTITY:-$GAME3_IDENTITY}" \
|
||||
POKEPORT_GBA_CACHE="${POKEPORT_GBA_CACHE:-$GAME3_GBA_CACHE}" "$LUA" tests/save_editor_touch_controls_test.lua
|
||||
|
||||
run_emerald_tier() {
|
||||
local t rc=0
|
||||
@@ -322,8 +371,10 @@ run_emerald_tier() {
|
||||
|| { echo "FAIL tests/game3_cart_random_sessions_test.lua (emerald)"; rc=1; }
|
||||
return $rc
|
||||
}
|
||||
ACTIVE_GROUP=game3
|
||||
run_tier "T6e emerald suites" run_emerald_tier
|
||||
|
||||
ACTIVE_GROUP=mods
|
||||
run_tier "T4 title checkpoint cold restart" \
|
||||
bash tests/integration/title_checkpoint_cold_start.sh
|
||||
|
||||
@@ -388,6 +439,7 @@ run_content_behavior() {
|
||||
|
||||
# The Red dataset is the source tree's when tools/build_data.py wrote one, and
|
||||
# otherwise the imported Red cache, through Data:load's POKEPORT_DATA_DIR hook.
|
||||
ACTIVE_GROUP=content
|
||||
HAVE_RED_DATA=0
|
||||
if [ -f data/generated/maps.lua ]; then
|
||||
HAVE_RED_DATA=1
|
||||
@@ -411,7 +463,6 @@ if [ "$HAVE_RED_DATA" = "1" ]; then
|
||||
run_tier "T3 save editor: events + dex" "$LUA" tests/save_editor_task7_tests.lua
|
||||
run_tier "T3 save editor: map browser" "$LUA" tests/save_editor_task8_tests.lua
|
||||
run_tier "T3 save editor: mod awareness" "$LUA" tests/save_editor_mod_tests.lua
|
||||
run_tier "T3 save editor: gold / gen2" "$LUA" tests/save_editor_gen2_tests.lua
|
||||
run_tier "T3 save editor: bag / PC move" "$LUA" tests/save_editor_item_move_bug1951_test.lua
|
||||
run_tier "T3 save editor: mobile properties" "$LUA" tests/save_editor_mobile_properties_tests.lua
|
||||
run_tier "T5 link (loopback lockstep)" "$LUA" tests/run_link_tests.lua
|
||||
@@ -488,6 +539,10 @@ fi
|
||||
|
||||
# ------- verdict
|
||||
|
||||
if [ "$LIST" = 1 ]; then
|
||||
echo "Listed $SELECTED_TIERS tiers for $GROUP (tests were not run)"
|
||||
exit 0
|
||||
fi
|
||||
echo ""
|
||||
echo "=============================================================="
|
||||
if [ ${#FAILED[@]} -eq 0 ]; then
|
||||
|
||||
@@ -1522,7 +1522,7 @@ end
|
||||
local TAB_ICONS = { mods = "puzzle", find = "search", online = "globe",
|
||||
skins = "paintbrush", importers = "download" }
|
||||
local TAB_LABELS = { mods = "MODS", find = "FIND", online = "ONLINE",
|
||||
skins = "SKINS", importers = "IMPORT" }
|
||||
skins = "SKINS", importers = "EXTRA" }
|
||||
for _, t in ipairs(HEADER_TABS) do
|
||||
t.opts = { face = "tab", font = "tab", icon = TAB_ICONS[t.id] }
|
||||
end
|
||||
|
||||
+13
-1
@@ -68,12 +68,24 @@ for _, atlas in ipairs(atlases) do
|
||||
end
|
||||
end
|
||||
|
||||
Icons.NAMES[#Icons.NAMES + 1] = "triangle-alert"
|
||||
function Icons.has(name)
|
||||
return cells[name] ~= nil
|
||||
return name == "triangle-alert" or cells[name] ~= nil
|
||||
end
|
||||
|
||||
function Icons.draw(name, x, y, size, color, alpha)
|
||||
local g = love and love.graphics
|
||||
if g and name == "triangle-alert" then
|
||||
g.push("all")
|
||||
g.setColor(color[1] / 255, color[2] / 255, color[3] / 255, alpha or 1)
|
||||
g.setLineWidth(math.max(1, size * 0.075))
|
||||
g.polygon("line", x + size * 0.5, y + size * 0.1,
|
||||
x + size * 0.08, y + size * 0.88, x + size * 0.92, y + size * 0.88)
|
||||
g.line(x + size * 0.5, y + size * 0.35, x + size * 0.5, y + size * 0.59)
|
||||
g.circle("fill", x + size * 0.5, y + size * 0.74, size * 0.045)
|
||||
g.pop()
|
||||
return
|
||||
end
|
||||
if not g or not g.newQuad or not g.newImage then
|
||||
return
|
||||
end
|
||||
|
||||
@@ -223,22 +223,56 @@ return function(game)
|
||||
shot(section, 390, 844)
|
||||
shot("full-label-" .. section, 320, 568)
|
||||
end
|
||||
if Gen.ofState(S) == 3 then
|
||||
for _, choice in ipairs({
|
||||
{ "main", "nature" },
|
||||
{ "origin", "language" },
|
||||
{ "origin", "pokeball" },
|
||||
{ "origin", "metGame" },
|
||||
}) do
|
||||
S.monSection, S.propertyChoice, S.inspectorScroll = choice[1], choice[2], 300
|
||||
shot("full-label-choice-" .. choice[2], 320, 568)
|
||||
end
|
||||
S.monSection, S.propertyChoice, S.inspectorScroll = "origin", "language", 500
|
||||
shot("language-choices", 390, 844, function()
|
||||
S.inspectorScroll = 500
|
||||
end)
|
||||
S.propertyChoice, S.inspectorScroll = nil, 0
|
||||
local Touch=require("TouchEditor")
|
||||
local mon=S.editingMon
|
||||
for _,size in ipairs({{320,568},{390,844},{640,360}}) do
|
||||
Touch.open(S,Kit,{mode="number",id="level",title="Level",value=mon.level,
|
||||
limits=function() return require("ValueLimits").mon(S,mon,"level") end,
|
||||
apply=function(v) return Ops.setLevel(S,mon,v) end})
|
||||
shot("value-wheel-"..size[1],size[1],size[2])
|
||||
S.editPopup.scroll=10000
|
||||
shot("value-wheel-footer-"..size[1],size[1],size[2])
|
||||
Touch.close(S,Kit)
|
||||
end
|
||||
if Gen.ofState(S)==3 then
|
||||
Touch.open(S,Kit,{mode="choice",title="Found at",value=mon.metLocation,
|
||||
options=require("NamedChoices").locations(S),apply=function(v) return Ops.setMonProperty(S,mon,"metLocation",v) end})
|
||||
shot("searchable-locations",390,844)
|
||||
S.editPopup.query="route"
|
||||
shot("searchable-locations-filtered",320,568)
|
||||
Touch.close(S,Kit)
|
||||
Ops.setEv(S,mon,"hp",255);Ops.setEv(S,mon,"atk",200)
|
||||
S.monSection,S.inspectorScroll="stats",0
|
||||
shot("ev-budget",390,844)
|
||||
Touch.open(S,Kit,{mode="number",title="Defense EV",value=0,
|
||||
limits=function() return require("ValueLimits").mon(S,mon,"ev-def") end,
|
||||
apply=function(v) return Ops.setEv(S,mon,"def",v) end})
|
||||
shot("ev-budget-wheel",390,844)
|
||||
Touch.close(S,Kit)
|
||||
end
|
||||
Touch.open(S,Kit,{mode="help",title="Randomize Pokémon",help="Replaces this Pokémon with a wild one from this game. Real level range, normal moves. Undo brings yours back."})
|
||||
shot("action-help",320,568)
|
||||
Touch.close(S,Kit)
|
||||
local originalMon = require("src.mods.Merge").deepCopy(mon)
|
||||
mon.level, mon.hp, mon.status = 105, -20, "UNKNOWN"
|
||||
if Gen.ofState(S) == 3 then
|
||||
mon.ivs.hp, mon.language = 99, 6
|
||||
mon.evs.hp, mon.evs.atk, mon.evs.def = 255, 255, 100
|
||||
else
|
||||
mon.dvs.attack = 25
|
||||
end
|
||||
S.monSection, S.inspectorScroll = "main", 0
|
||||
shot("invalid-saved-main", 390, 844)
|
||||
S.monSection, S.inspectorScroll = "stats", 0
|
||||
shot("invalid-saved-stats", 390, 844)
|
||||
if Gen.ofState(S) == 3 then
|
||||
S.monSection, S.inspectorScroll = "origin", 0
|
||||
shot("invalid-saved-origin", 320, 568)
|
||||
S.inspectorScroll = 300
|
||||
shot("invalid-saved-language", 320, 568)
|
||||
end
|
||||
for k in pairs(mon) do mon[k] = nil end
|
||||
for k, v in pairs(originalMon) do mon[k] = v end
|
||||
S.monSection = "main"
|
||||
shot("selected-buttons", 390, 844)
|
||||
shot("rounded-buttons", 390, 844)
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if package.config:sub(1, 1) == "\\" then
|
||||
print("[skip] Ubuntu CI runner requires a POSIX shell")
|
||||
os.exit(0)
|
||||
end
|
||||
local T = require("tests.harness")
|
||||
local Runner = require("tests.tier_runner")
|
||||
local Groups = require("tests.save_compat._groups")
|
||||
|
||||
local function capture(command)
|
||||
local pipe = assert(io.popen(command))
|
||||
local text = pipe:read("*a")
|
||||
pipe:close()
|
||||
return text
|
||||
end
|
||||
|
||||
local names = {}
|
||||
for name in capture("bash scripts/test.sh --list-groups"):gmatch("[^\r\n]+") do
|
||||
names[#names + 1] = name
|
||||
end
|
||||
T.eq(#names, 8, "eight named ROM-free groups")
|
||||
local workflow = assert(io.open(".github/workflows/ci.yml", "rb"))
|
||||
local text = workflow:read("*a")
|
||||
workflow:close()
|
||||
local aggregate = assert(text:match(" headless:\n(.-) headless%-groups:"))
|
||||
T.check(aggregate:find("name: headless suites (no ROM)", 1, true), "required branch check keeps its name")
|
||||
T.check(aggregate:find("needs: headless-groups", 1, true), "required check waits for every matrix child")
|
||||
T.check(aggregate:find("if: ${{ always() }}", 1, true), "required check runs after failed or skipped groups")
|
||||
T.check(aggregate:find("HEADLESS_RESULT: ${{ needs.headless-groups.result }}", 1, true),
|
||||
"required check reads the combined matrix result")
|
||||
local gate = assert(aggregate:match(" run: |\n(.*)")):gsub("\n ", "\n"):gsub("^ ", "")
|
||||
local gatePath = os.tmpname()
|
||||
local gateFile = assert(io.open(gatePath, "wb"))
|
||||
gateFile:write(gate)
|
||||
gateFile:close()
|
||||
for _, status in ipairs({ "success", "failure", "cancelled", "skipped" }) do
|
||||
local result = os.execute("HEADLESS_RESULT=" .. status .. " bash " .. gatePath .. " >/dev/null 2>&1")
|
||||
T.eq(result == 0 or result == true, status == "success", "required check handles " .. status)
|
||||
end
|
||||
os.remove(gatePath)
|
||||
local headless = assert(text:match(" headless%-groups:\n(.-) fixture%-dataset:"))
|
||||
local configured = {}
|
||||
for name in headless:gmatch("%- group: ([%w%-]+)") do
|
||||
T.check(not configured[name], "CI group occurs once: " .. name)
|
||||
configured[name] = true
|
||||
end
|
||||
T.check(headless:find('run: ./scripts/test.sh --group "${{ matrix.group }}"', 1, true),
|
||||
"CI executes the selected group")
|
||||
T.check(headless:find("fail-fast: false", 1, true), "one failing group does not cancel the others")
|
||||
|
||||
local prefix = "POKEPORT_TEST_CACHES=/tmp/no-headless-group-cache POKEPORT_IDENTITY=ci-group-plan "
|
||||
.. "RED_CACHE= BLUE_CACHE= YELLOW_CACHE= GOLD_CACHE= SILVER_CACHE= CRYSTAL_CACHE= "
|
||||
local function plan(group)
|
||||
local result = {}
|
||||
local output = capture(prefix .. "bash scripts/test.sh --quick --list --group " .. group)
|
||||
T.check(output:find("tests were not run", 1, true), "listing does not execute tests: " .. group)
|
||||
for label in output:gmatch("%[tier%] ([^\r\n]+)") do
|
||||
T.check(not result[label], "tier occurs once in " .. group .. ": " .. label)
|
||||
result[label] = true
|
||||
end
|
||||
return result
|
||||
end
|
||||
local all, assigned = plan("all"), {}
|
||||
for _, name in ipairs(names) do
|
||||
T.check(configured[name], "CI includes " .. name)
|
||||
configured[name] = nil
|
||||
local selected = plan(name)
|
||||
T.check(next(selected) ~= nil, "group is not empty: " .. name)
|
||||
for label in pairs(selected) do
|
||||
T.check(all[label], "group tier belongs to the full suite: " .. label)
|
||||
T.check(not assigned[label], "tier belongs to exactly one CI group: " .. label)
|
||||
assigned[label] = name
|
||||
end
|
||||
end
|
||||
T.check(next(configured) == nil, "CI has no unknown groups")
|
||||
for label in pairs(all) do
|
||||
T.check(assigned[label], "full ROM-free tier is covered in CI: " .. label)
|
||||
end
|
||||
|
||||
local counts, seen = {}, {}
|
||||
for _, path in ipairs(Runner.suites("tests/save_compat")) do
|
||||
local group = Groups.of(path)
|
||||
T.check(Groups.valid[group], "every save suite has a group: " .. path)
|
||||
counts[group] = (counts[group] or 0) + 1
|
||||
seen[path:match("([^/]+)%.lua$")] = true
|
||||
end
|
||||
for name in pairs(Groups.named) do
|
||||
T.check(seen[name], "named save suite exists: " .. name)
|
||||
end
|
||||
for group in pairs(Groups.valid) do
|
||||
T.check((counts[group] or 0) > 0, "save group is not empty: " .. group)
|
||||
end
|
||||
T.eq(Groups.of("tests/save_compat/future_codec_test.lua"), "codecs", "new save tests remain covered")
|
||||
local invalid = os.execute("bash scripts/test.sh --group nonexistent >/dev/null 2>&1")
|
||||
T.check(invalid ~= 0 and invalid ~= true, "unknown group fails instead of running the full suite")
|
||||
invalid = os.execute("luajit tests/run_save_compat.lua --group nonexistent >/dev/null 2>&1")
|
||||
T.check(invalid ~= 0 and invalid ~= true, "unknown save group fails")
|
||||
|
||||
-- Exercise real child-process success, failure and an empty filter.
|
||||
local dir = os.tmpname()
|
||||
os.remove(dir)
|
||||
assert(os.execute("mkdir -p " .. dir) == 0)
|
||||
for name, exitCode in pairs({ pass = 0, fail = 7 }) do
|
||||
local file = assert(io.open(dir .. "/" .. name .. ".lua", "wb"))
|
||||
file:write("exit " .. exitCode .. "\n")
|
||||
file:close()
|
||||
end
|
||||
local oldInterpreter, oldPrint = arg[-1], print
|
||||
arg[-1], print = "sh", function() end
|
||||
local passed, passedTotal = Runner.run({ dir }, "filtered pass", function(p) return p:match("/pass%.lua$") end)
|
||||
local failed, failedTotal = Runner.run({ dir }, "filtered fail", function(p) return p:match("/fail%.lua$") end)
|
||||
local empty = Runner.run({ dir }, "empty", function() return false end)
|
||||
arg[-1], print = oldInterpreter, oldPrint
|
||||
T.eq(passed, 0, "successful selected child passes")
|
||||
T.eq(passedTotal, 1, "unselected failing child is not run")
|
||||
T.eq(failed, 1, "selected child failure propagates")
|
||||
T.eq(failedTotal, 1, "failure reports the selected count")
|
||||
T.eq(empty, 1, "empty group cannot pass vacuously")
|
||||
os.remove(dir .. "/pass.lua")
|
||||
os.remove(dir .. "/fail.lua")
|
||||
os.execute("rmdir " .. dir)
|
||||
T.finish("headless CI groups")
|
||||
@@ -1,3 +1,14 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
require("tests.tier_runner").main({ "tests/save_compat" }, "save_compat")
|
||||
local Runner = require("tests.tier_runner")
|
||||
local Groups = require("tests.save_compat._groups")
|
||||
if not arg[1] then
|
||||
Runner.main({ "tests/save_compat" }, "save_compat")
|
||||
elseif arg[1] == "--group" and Groups.valid[arg[2]] and not arg[3] then
|
||||
local group = arg[2]
|
||||
Runner.main({ "tests/save_compat" }, "save_compat/" .. group,
|
||||
function(path) return Groups.of(path) == group end)
|
||||
else
|
||||
io.stderr:write("usage: tests/run_save_compat.lua [--group codecs|records|world]\n")
|
||||
os.exit(2)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
local Groups = {}
|
||||
|
||||
Groups.valid = { codecs = true, records = true, world = true }
|
||||
Groups.named = {
|
||||
gen3_sec_frextra_test = "records",
|
||||
gen3_sec_records_test = "records",
|
||||
gen3_sec_town_test = "world",
|
||||
gen3_sec_tv_test = "world",
|
||||
}
|
||||
|
||||
function Groups.of(path)
|
||||
local name = path:match("([^/]+)%.lua$")
|
||||
-- New codec suites stay covered without updating a hand-maintained list.
|
||||
return Groups.named[name] or "codecs"
|
||||
end
|
||||
|
||||
return Groups
|
||||
@@ -746,7 +746,9 @@ do
|
||||
local labels = {}
|
||||
for _, r in ipairs(Kit.audit) do labels[r.label] = true end
|
||||
Kit.audit = nil
|
||||
check(labels["property-caughtTime"] ~= nil, "the Crystal origin form offers the caught time")
|
||||
local timeChoice=false
|
||||
for label in pairs(labels) do if label:match("^Time found:") then timeChoice=true end end
|
||||
check(timeChoice, "the Crystal origin form offers named caught times")
|
||||
S.inspectorScroll=300
|
||||
Kit.audit={}
|
||||
MonEditor.draw(S,Kit,0,0,1280,720)
|
||||
@@ -887,5 +889,37 @@ do
|
||||
end
|
||||
end
|
||||
|
||||
for _, version in ipairs({ "gold", "silver", "crystal" }) do
|
||||
local S = newState(version)
|
||||
S.data = require("src.mods.Merge").deepCopy(data)
|
||||
S.data.maps = { ROUTE_29 = { landmark = 3 } }
|
||||
S.data.encounters = {
|
||||
grass = { ROUTE_29 = { slots = { NITE = { { species = "TOTODILE", level = 5 } } } } },
|
||||
fishGroups = { NO_MAP = { { species = "CYNDAQUIL", level = 90 } } },
|
||||
}
|
||||
Ops.partyAdd(S)
|
||||
local mon = S.save.party[1]
|
||||
Ops.setHeldItem(S, mon, "FLOWER_MAIL")
|
||||
mon.level, mon.hp, mon.dvs.attack, mon.statExp.hp, mon.pokerus = 110, -10, 20, 999999, 255
|
||||
check(Ops.fixMonErrors(S, mon), version .. " repairs malformed properties")
|
||||
eq(require("Legality").mon(S, mon).errors, 0, version .. " repaired values pass validator")
|
||||
check(Ops.maxMon(S, mon), version .. " max out succeeds")
|
||||
eq(mon.level, 100, version .. " max reaches level 100")
|
||||
eq(mon.happiness, 255, version .. " max fills friendship")
|
||||
eq(mon.statExp.hp, 65535, version .. " max fills stat experience")
|
||||
eq(mon.dvs.attack, 15, version .. " max fills DVs")
|
||||
eq(require("Legality").mon(S, mon).errors, 0, version .. " maxed values pass validator")
|
||||
check(Ops.randomizeMon(S, mon), version .. " randomize succeeds with native DV properties")
|
||||
eq(mon.species, "TOTODILE", version .. " randomize uses available encounters")
|
||||
eq(mon.level, 5, version .. " randomize uses encounter level")
|
||||
if version == "crystal" then
|
||||
eq(mon.caughtLocation, 3, "Crystal randomize keeps the encounter landmark")
|
||||
eq(mon.caughtTime, 3, "Crystal randomize keeps the nighttime slot")
|
||||
end
|
||||
eq(require("Legality").mon(S, mon).errors, 0, version .. " random values pass validator")
|
||||
eq(require("src.core.gen2.Mail").state(S.save).party[1], nil,
|
||||
version .. " randomize clears the replaced held mail")
|
||||
end
|
||||
|
||||
print(string.format("save editor gen2 tests: %d passed, %d failed", passed, failed))
|
||||
if failed > 0 then os.exit(1) end
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
package.path = package.path .. ";./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
|
||||
_G.love = require("tests.love_stub")
|
||||
local native = os.getenv("POKEPORT_TOUCH_NATIVE") == "1"
|
||||
local version = os.getenv("POKEPORT_TOUCH_VERSION") or (native and "firered" or "red")
|
||||
if native then
|
||||
require("src.core.GameVersion").set(version)
|
||||
require("src.import.gba.versions").select(version)
|
||||
end
|
||||
if native then
|
||||
require("tests.game3_cache").mountOrSkip("save_editor_touch_controls_test", "pokemon/meta.lua")
|
||||
end
|
||||
local App = require("tools.save-editor.App")
|
||||
local Ops, Gen, Kit = require("Ops"), require("Gen"), require("Kit")
|
||||
local Touch, Limits, Actions = require("TouchEditor"), require("ValueLimits"), require("MonActions")
|
||||
local Copy = require("src.mods.Merge").deepCopy
|
||||
local SD = require("src.core.SaveData")
|
||||
local path = os.tmpname() .. "-touch.lua"
|
||||
local f = assert(io.open(path, "wb"))
|
||||
f:write(SD.encode(Gen.newGame(version)))
|
||||
f:close()
|
||||
App.load(path, { version = version, embedded = true })
|
||||
local S = App.getState()
|
||||
assert(S.save and not S.loadError, S.status)
|
||||
Ops.partyAdd(S)
|
||||
Ops.selectParty(S, 1)
|
||||
local mon = S.editingMon
|
||||
local count = 0
|
||||
local function check(ok, msg)
|
||||
count = count + 1
|
||||
assert(ok, msg)
|
||||
end
|
||||
-- Fractional wheels and a short drag preserve their precise pixel distance.
|
||||
Kit.layout(360, 640)
|
||||
Kit.beginFrame(50, 50, false, -0.25)
|
||||
local state = { offset = 0 }
|
||||
local _, shift = Kit.list(state, "offset", 0, 0, 200, 150, 30, 60)
|
||||
check(shift == 12 * Kit.scale, "partial wheel does not jump a whole row")
|
||||
Kit.endFrame()
|
||||
Kit._touchDrag = { x = 50, startY = 50 }
|
||||
Kit.touchDown = true
|
||||
Kit.beginFrame(50, 33, false, 0)
|
||||
Kit.dragAdd(17)
|
||||
Kit.list(state, "offset", 0, 0, 200, 150, 30, 60)
|
||||
check(math.abs(state._listState.offset.pixels - (12 * Kit.scale + 17)) < 0.001, "17px drag moves list 17px")
|
||||
Kit.endFrame()
|
||||
Kit.touchDown = false
|
||||
-- Real App touch events drive the modal, including SDL's synthesized hold.
|
||||
local oldDimensions, oldSafe, oldDown = love.graphics.getDimensions, love.window.getSafeArea, love.mouse.isDown
|
||||
love.graphics.getDimensions = function()
|
||||
return 360, 640
|
||||
end
|
||||
love.window.getSafeArea = function()
|
||||
return 0, 0, 360, 640
|
||||
end
|
||||
local held = false
|
||||
love.mouse.isDown = function()
|
||||
return held
|
||||
end
|
||||
S.tab, S.mobileInspector, S.monSection = "party", true, "main"
|
||||
local initialLevel = mon.level
|
||||
local function levelPopup()
|
||||
Touch.open(S, Kit, {
|
||||
mode = "number",
|
||||
title = "Level",
|
||||
value = initialLevel,
|
||||
limits = function()
|
||||
return Limits.mon(S, mon, "level")
|
||||
end,
|
||||
apply = function(v)
|
||||
return Ops.setLevel(S, mon, v)
|
||||
end,
|
||||
})
|
||||
App.draw()
|
||||
App.draw()
|
||||
end
|
||||
levelPopup()
|
||||
local rect = S.editPopup.sliderRect
|
||||
held = true
|
||||
App.touchpressed("slider", rect.x + rect.w * 0.7, rect.y + rect.h / 2)
|
||||
App.draw()
|
||||
App.touchmoved("slider", rect.x + rect.w * 0.9, rect.y + rect.h / 2)
|
||||
App.draw()
|
||||
check(S.editPopup.value > initialLevel and mon.level == initialLevel, "touch slider previews without mutating")
|
||||
App.touchreleased("slider", rect.x + rect.w * 0.9, rect.y + rect.h / 2)
|
||||
App.draw()
|
||||
check(S.editPopup ~= nil, "synthetic mouse hold does not activate behind modal")
|
||||
held = false
|
||||
App.keypressed("escape")
|
||||
App.draw()
|
||||
levelPopup()
|
||||
rect = S.editPopup.wheelRect
|
||||
for _ = 1, 4 do
|
||||
Kit.beginFrame(rect.x + rect.w / 2, rect.y + rect.h / 2, false, 0.25)
|
||||
Touch.draw(S, Kit, 360, 640)
|
||||
Kit.endFrame()
|
||||
end
|
||||
check(S.editPopup.value == initialLevel + 1, "fractional wheel motion accumulates whole values")
|
||||
App.touchpressed("wheel", rect.x + rect.w / 2, rect.y + rect.h / 2)
|
||||
App.draw()
|
||||
App.touchmoved("wheel", rect.x + rect.w / 2 - 50, rect.y + rect.h / 2)
|
||||
App.draw()
|
||||
check(S.editPopup.value > initialLevel and mon.level == initialLevel, "touch number wheel fine-tunes a draft")
|
||||
App.touchreleased("wheel", rect.x + rect.w / 2 - 50, rect.y + rect.h / 2)
|
||||
App.draw()
|
||||
App.mousepressed(1, 1, 1)
|
||||
App.draw()
|
||||
check(not S.editPopup and mon.level == initialLevel, "outside tap cancels a dragged value")
|
||||
levelPopup()
|
||||
S.editPopup.typing, S.editPopup.draft = true, "999"
|
||||
check(not Touch.commit(S, Kit) and S.editPopup.error ~= nil, "invalid exact entry stays open with a range error")
|
||||
Kit.audit = {}
|
||||
App.draw()
|
||||
local maxButton
|
||||
for _, control in ipairs(Kit.audit) do
|
||||
if control.label == "Max" then
|
||||
maxButton = control
|
||||
end
|
||||
end
|
||||
Kit.audit = nil
|
||||
assert(maxButton, "numeric popup has a Max button")
|
||||
local maxX, maxY = maxButton.x + maxButton.w / 2, maxButton.y + maxButton.h / 2
|
||||
App.touchpressed("max", maxX, maxY)
|
||||
App.touchreleased("max", maxX, maxY)
|
||||
App.draw()
|
||||
check(
|
||||
not S.editPopup.typing and not S.editPopup.error and S.editPopup.value == 100,
|
||||
"Max replaces an invalid typed draft and clears its error"
|
||||
)
|
||||
check(Touch.commit(S, Kit) and mon.level == 100, "Apply uses the tapped Max instead of stale text")
|
||||
require("History").undo(S)
|
||||
Ops.selectParty(S, 1)
|
||||
mon = S.editingMon
|
||||
Touch.close(S, Kit)
|
||||
love.graphics.getDimensions, love.window.getSafeArea, love.mouse.isDown = oldDimensions, oldSafe, oldDown
|
||||
-- Changing a slider is only a draft. Outside/escape cancel it; Apply commits once.
|
||||
local oldLevel = mon.level
|
||||
Touch.open(S, Kit, {
|
||||
mode = "number",
|
||||
title = "Level",
|
||||
value = oldLevel,
|
||||
limits = function()
|
||||
return Limits.mon(S, mon, "level")
|
||||
end,
|
||||
apply = function(v)
|
||||
return Ops.setLevel(S, mon, v)
|
||||
end,
|
||||
})
|
||||
Touch.keypressed(S, Kit, "up")
|
||||
check(mon.level == oldLevel, "wheel draft does not change save")
|
||||
Touch.keypressed(S, Kit, "escape")
|
||||
check(mon.level == oldLevel and not S.editPopup, "escape cancels draft")
|
||||
local before = #S.undoStack
|
||||
Touch.open(S, Kit, {
|
||||
mode = "number",
|
||||
title = "Level",
|
||||
value = oldLevel + 5,
|
||||
limits = function()
|
||||
return Limits.mon(S, mon, "level")
|
||||
end,
|
||||
apply = function(v)
|
||||
return Ops.setLevel(S, mon, v)
|
||||
end,
|
||||
})
|
||||
check(Touch.commit(S, Kit) and mon.level == oldLevel + 5, "apply changes level by five")
|
||||
check(#S.undoStack == math.min(require("History").LIMIT, before + 1), "value apply has one undo snapshot")
|
||||
require("History").undo(S)
|
||||
Ops.selectParty(S, 1)
|
||||
mon = S.editingMon
|
||||
check(mon.level == oldLevel, "undo restores coupled level/stats/experience")
|
||||
-- Named results never depend on the old bare number labels.
|
||||
Touch.open(S, Kit, {
|
||||
mode = "choice",
|
||||
title = "Status",
|
||||
options = { { "healthy", "Healthy" }, { "PSN", "Poisoned" } },
|
||||
apply = function(v)
|
||||
return Ops.setMonStatus(S, mon, v)
|
||||
end,
|
||||
})
|
||||
S.editPopup.query = "poison"
|
||||
S.editPopup.index = 2
|
||||
check(#Touch.results(S.editPopup) == 1, "named popup filters")
|
||||
check(Touch.commit(S, Kit) and mon.status == "PSN", "search commits a named choice")
|
||||
-- Corrupted properties repair together and metadata is retained.
|
||||
mon.level, mon.hp, mon.status = 105, -50, "garbage"
|
||||
mon.nickname = "ABCDEFGHIJKLMNO"
|
||||
mon.customPreserved = { note = "keep me" }
|
||||
if native then
|
||||
mon.ivs.hp = 99
|
||||
mon.evs = { hp = 255, atk = 255, def = 255 }
|
||||
mon.language = 6
|
||||
mon.metLevel = 120
|
||||
else
|
||||
mon.dvs.attack = 99
|
||||
mon.dvs.hp = 0
|
||||
mon.statExp.hp = 999999
|
||||
end
|
||||
local Paint, PAL = require("src.ui.kit.Button"), require("Theme").PAL
|
||||
local realPaint, marked = Paint.draw, {}
|
||||
Paint.draw = function(owner, x, y, w, h, label, opts, hot, focused)
|
||||
if owner == Kit and opts.invalid and opts.id then
|
||||
marked[opts.id] = opts
|
||||
check(opts.ink == PAL.red and opts.stroke == PAL.red, "invalid control uses red ink and outline")
|
||||
end
|
||||
return realPaint(owner, x, y, w, h, label, opts, hot, focused)
|
||||
end
|
||||
local unchanged = SD.encode(S.save)
|
||||
for _, section in ipairs({ "main", "stats", "origin" }) do
|
||||
S.monSection, S.inspectorScroll = section, 0
|
||||
App.draw()
|
||||
end
|
||||
check(marked["value-level"] ~= nil, "saved invalid level is visibly marked")
|
||||
check(marked["choice-status"] ~= nil, "saved invalid choice is visibly marked")
|
||||
check(marked[native and "value-iv-hp" or "value-dv-attack"] ~= nil, "saved invalid stat is visibly marked")
|
||||
if native then
|
||||
check(marked["choice-language"] ~= nil, "unsupported saved language is visibly marked")
|
||||
check(marked["value-ev-spa"] ~= nil, "shared EV limit marks affected controls")
|
||||
end
|
||||
check(SD.encode(S.save) == unchanged, "highlighting never changes the saved values")
|
||||
local highlights = require("Legality").highlights(require("Legality").mon(S, mon), mon)
|
||||
check(highlights.sections.main > 0 and highlights.sections.stats > 0, "sections expose their error counts")
|
||||
before = #S.undoStack
|
||||
local ok = Ops.fixMonErrors(S, mon)
|
||||
check(ok, S.status)
|
||||
marked = {}
|
||||
for _, section in ipairs({ "main", "stats", "origin" }) do
|
||||
S.monSection, S.inspectorScroll = section, 0
|
||||
App.draw()
|
||||
end
|
||||
check(next(marked) == nil, "red issue styling clears after repair while origin stays unchecked")
|
||||
local coupled = Copy(mon)
|
||||
require("MonOps").setLevel(S.data, coupled, 50, Gen.ofState(S))
|
||||
coupled.metLevel = 0
|
||||
local wrongExp = Limits.expAt(S, coupled, 50) - 1
|
||||
coupled.exp, coupled.experience = wrongExp, wrongExp
|
||||
local coupledIssues = require("Legality").highlights(require("Legality").mon(S, coupled), coupled)
|
||||
check(coupledIssues.fields.experience ~= nil, "experience/level mismatch marks the experience field")
|
||||
marked = {}
|
||||
Kit.beginFrame(-1, -1, false, 0)
|
||||
Touch.value(S, Kit, "experience", "Experience", wrongExp, function()
|
||||
return Limits.mon(S, coupled, "experience")
|
||||
end, 0, 0, 280, function() end, coupledIssues.fields.experience)
|
||||
Kit.endFrame()
|
||||
check(marked["value-experience"] ~= nil, "in-range number with a coupled error still paints red")
|
||||
Paint.draw = realPaint
|
||||
check(require("Legality").mon(S, mon).errors == 0, "repair passes actual property validator")
|
||||
check(mon.customPreserved.note == "keep me", "repair preserves unrelated metadata")
|
||||
check(#S.undoStack == math.min(require("History").LIMIT, before + 1), "repair is one undo")
|
||||
require("History").undo(S)
|
||||
Ops.selectParty(S, 1)
|
||||
mon = S.editingMon
|
||||
check(mon.level == 105 and mon.hp == -50, "repair undo restores original bad values")
|
||||
Ops.fixMonErrors(S, mon)
|
||||
before = #S.undoStack
|
||||
check(Ops.maxMon(S, mon), S.status)
|
||||
check(mon.level == 100 and mon.hp == (mon.maxHp or mon.stats.hp), "max sets level and fills resulting HP")
|
||||
check(require("Legality").mon(S, mon).errors == 0, "max passes actual property validator")
|
||||
check(#S.undoStack == math.min(require("History").LIMIT, before + 1), "max is one undo")
|
||||
if native then
|
||||
Ops.clearEvs(S, mon)
|
||||
Ops.setEv(S, mon, "hp", 255)
|
||||
Ops.setEv(S, mon, "atk", 255)
|
||||
check(Limits.mon(S, mon, "ev-def").hi == 0, "full EV budget visibly caps another stat")
|
||||
Ops.setEv(S, mon, "atk", 200)
|
||||
check(Limits.mon(S, mon, "ev-def").hi == 55, "lowering EVs exposes newly available max")
|
||||
local hp = Limits.mon(S, mon, "current-hp").hi
|
||||
Ops.setLevel(S, mon, 50)
|
||||
check(Limits.mon(S, mon, "current-hp").hi < hp, "HP cap follows level")
|
||||
Ops.setPpUps(S, mon, 1, 0)
|
||||
local pp = Limits.mon(S, mon, "pp-1").hi
|
||||
Ops.setPpUps(S, mon, 1, 3)
|
||||
check(Limits.mon(S, mon, "pp-1").hi > pp, "PP cap follows PP Ups")
|
||||
local locations = require("NamedChoices").locations(S)
|
||||
local found = false
|
||||
for _, entry in ipairs(locations) do
|
||||
if entry[2]:lower():find("route", 1, true) then
|
||||
found = true
|
||||
end
|
||||
end
|
||||
check(found, "native locations come from named ROM sections")
|
||||
end
|
||||
local candidates = Actions.encounters(S)
|
||||
check(#candidates > 0, "wild encounter recipes exist for loaded game")
|
||||
local beforeMon = Copy(mon)
|
||||
local rng = native and require("src.core.game3.rng").getState()
|
||||
before = #S.undoStack
|
||||
check(Ops.randomizeMon(S, mon), S.status)
|
||||
check(require("Legality").mon(S, mon).errors == 0, "randomized Pokémon passes properties")
|
||||
check(require("Legality").mon(S, mon).status == "unchecked", "randomization does not falsely certify origin")
|
||||
check(#S.undoStack == math.min(require("History").LIMIT, before + 1), "randomization is one undo")
|
||||
if native then
|
||||
local now = require("src.core.game3.rng").getState()
|
||||
check(
|
||||
now.value == rng.value and now.value2 == rng.value2 and now.wild == rng.wild,
|
||||
"randomization does not advance gameplay RNG"
|
||||
)
|
||||
end
|
||||
require("History").undo(S)
|
||||
Ops.selectParty(S, 1)
|
||||
mon = S.editingMon
|
||||
check(mon.species == beforeMon.species and mon.level == beforeMon.level, "randomize undo restores original")
|
||||
-- Save-wide repair fixes party and box entries without skipping box slot gaps.
|
||||
Ops.cloneMonToBox(S, mon)
|
||||
S.save.party[1].level = -2
|
||||
S.editingMon.hp = -5
|
||||
before = #S.undoStack
|
||||
check(Ops.fixAllErrors(S), S.status)
|
||||
check(require("Legality").save(S).errors == 0, "fix all repairs party and boxes")
|
||||
check(#S.undoStack == math.min(require("History").LIMIT, before + 1), "fix all is one undo")
|
||||
if native or Gen.ofState(S) == 2 then
|
||||
for i = 1, 100 do
|
||||
check(Ops.randomizeMon(S, S.save.party[1]), S.status)
|
||||
check(require("Legality").mon(S, S.save.party[1]).errors == 0, "random recipe passes validator " .. i)
|
||||
end
|
||||
end
|
||||
check(App.save(), "bulk-edited save writes")
|
||||
check(App.reload(), "bulk-edited save reloads")
|
||||
S = App.getState()
|
||||
check(require("Legality").save(S).errors == 0, "bulk edits survive save round trip")
|
||||
os.remove(path)
|
||||
print("save editor touch controls: " .. count .. " checks passed (" .. version .. ")")
|
||||
+17
-11
@@ -37,31 +37,37 @@ end
|
||||
|
||||
-- runs every suite in `dirs`, prints one line per suite, returns the
|
||||
-- number that failed
|
||||
function Runner.run(dirs, label)
|
||||
function Runner.run(dirs, label, include)
|
||||
local lua = interpreter()
|
||||
local failed, total = 0, 0
|
||||
|
||||
for _, dir in ipairs(dirs) do
|
||||
for _, path in ipairs(Runner.suites(dir)) do
|
||||
total = total + 1
|
||||
local status = os.execute(("%s %s"):format(lua, path))
|
||||
local ok = status == 0 or status == true
|
||||
if ok then
|
||||
print("ok " .. path)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("FAIL " .. path)
|
||||
if not include or include(path) then
|
||||
total = total + 1
|
||||
local status = os.execute(("%s %s"):format(lua, path))
|
||||
local ok = status == 0 or status == true
|
||||
if ok then
|
||||
print("ok " .. path)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("FAIL " .. path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if total == 0 then
|
||||
print("FAIL no suites selected for " .. label)
|
||||
return 1, 0
|
||||
end
|
||||
print(("\n%s: %d/%d suites passed"):format(label, total - failed, total))
|
||||
print(("%s"):format(failed == 0 and "ALL TESTS PASSED" or failed .. " FAILURES"))
|
||||
return failed, total
|
||||
end
|
||||
|
||||
function Runner.main(dirs, label)
|
||||
local failed = Runner.run(dirs, label)
|
||||
function Runner.main(dirs, label, include)
|
||||
local failed = Runner.run(dirs, label, include)
|
||||
os.exit(failed == 0 and 0 or 1)
|
||||
end
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ local clickX, clickY
|
||||
-- Wheel notches queued by App.wheelmoved since the last draw, handed to Kit
|
||||
-- there like mouseClicked is: LOVE delivers events before love.draw, so a
|
||||
-- notch is always spent by the frame that follows it (#595).
|
||||
local TouchEditor = require("TouchEditor")
|
||||
local wheelY = 0
|
||||
local touch
|
||||
|
||||
@@ -135,7 +136,8 @@ local function applyLoaded(path, statusVerb)
|
||||
S.formMon, S.nicknameMon = nil, nil
|
||||
S.monDrafts, S.trainerDrafts, S.walletDrafts = {}, {}, {}
|
||||
S.propertyChoice, S.itemMenu = nil, nil
|
||||
S.navPopup = nil
|
||||
S.navPopup, S.editPopup = nil, nil
|
||||
S._listState = nil
|
||||
S.mobileInspector = nil
|
||||
S._quitArmed = false
|
||||
S._openArmed = false
|
||||
@@ -305,7 +307,8 @@ end
|
||||
function App.unload()
|
||||
Motion.reset()
|
||||
touch = nil
|
||||
Kit.touchDown = nil
|
||||
Kit.touchDown, Kit.ignoreMouseDown = nil, nil
|
||||
Kit._touchDrag, Kit._pointerDrag, Kit._tapPending, Kit._dragDelta = nil, nil, nil, 0
|
||||
S = nil
|
||||
mods = nil
|
||||
App.dataVersion = nil
|
||||
@@ -346,19 +349,25 @@ local function handlePadAction(action)
|
||||
local mx, my = PadInput.pointer()
|
||||
App.mousepressed(mx, my, 1)
|
||||
elseif action == "b" then
|
||||
if S.navPopup then
|
||||
if S.editPopup then
|
||||
TouchEditor.close(S, Kit)
|
||||
elseif S.navPopup then
|
||||
Chooser.close(S)
|
||||
else
|
||||
App.close()
|
||||
end
|
||||
elseif action == "tab_prev" then
|
||||
if S.navPopup then
|
||||
if S.editPopup then
|
||||
TouchEditor.keypressed(S, Kit, S.editPopup.mode == "number" and "left" or "up")
|
||||
elseif S.navPopup then
|
||||
Chooser.keypressed(S, "up")
|
||||
else
|
||||
cycleTab(-1)
|
||||
end
|
||||
elseif action == "tab_next" then
|
||||
if S.navPopup then
|
||||
if S.editPopup then
|
||||
TouchEditor.keypressed(S, Kit, S.editPopup.mode == "number" and "right" or "down")
|
||||
elseif S.navPopup then
|
||||
Chooser.keypressed(S, "down")
|
||||
else
|
||||
cycleTab(1)
|
||||
@@ -491,6 +500,9 @@ function App.update(dt)
|
||||
end
|
||||
|
||||
function App.mousepressed(x, y, button)
|
||||
if touch then
|
||||
return
|
||||
end
|
||||
if button == 1 then
|
||||
mouseClicked = true
|
||||
clickX, clickY = x, y
|
||||
@@ -506,6 +518,10 @@ function App.touchpressed(id, x, y)
|
||||
end
|
||||
touch = { id = id, x = x, y = y, startX = x, startY = y, moved = false }
|
||||
Kit.touchDown = true
|
||||
Kit.ignoreMouseDown = true
|
||||
Kit._pointerDrag, Kit._tapPending = nil, nil
|
||||
Kit._touchDrag = { x = x, startY = y }
|
||||
Kit._dragDelta = 0
|
||||
PadInput.yieldToPointer()
|
||||
end
|
||||
|
||||
@@ -513,10 +529,14 @@ function App.touchmoved(id, x, y)
|
||||
if not touch or touch.id ~= id then
|
||||
return
|
||||
end
|
||||
local lastY = touch.y
|
||||
touch.x, touch.y = x, y
|
||||
if math.abs(x - touch.startX) + math.abs(y - touch.startY) > 10 then
|
||||
touch.moved = true
|
||||
end
|
||||
if touch.moved then
|
||||
Kit.dragAdd(lastY - y)
|
||||
end
|
||||
end
|
||||
|
||||
function App.touchreleased(id, x, y)
|
||||
@@ -525,10 +545,11 @@ function App.touchreleased(id, x, y)
|
||||
end
|
||||
App.touchmoved(id, x, y)
|
||||
if not touch.moved then
|
||||
App.mousepressed(x, y, 1)
|
||||
mouseClicked, clickX, clickY = true, x, y
|
||||
end
|
||||
touch = nil
|
||||
Kit.touchDown = false
|
||||
Kit.ignoreMouseDown = true
|
||||
end
|
||||
|
||||
function App.textinput(text)
|
||||
@@ -804,6 +825,7 @@ function App.draw()
|
||||
or (S.itemPicker ~= nil)
|
||||
or (S.movePicker ~= nil)
|
||||
or (S.navPopup ~= nil)
|
||||
or (S.editPopup ~= nil)
|
||||
or Motion.active()
|
||||
|
||||
Theme.field(width, height)
|
||||
@@ -898,7 +920,9 @@ function App.draw()
|
||||
Kit.blockClicks = false
|
||||
-- Scrim still covers the full window (including unsafe bands); the card
|
||||
-- itself is centred in the safe rect so search fields clear the notch.
|
||||
if S.navPopup then
|
||||
if S.editPopup then
|
||||
TouchEditor.draw(S, Kit, width, height)
|
||||
elseif S.navPopup then
|
||||
Chooser.draw(S, Kit, width, height)
|
||||
else
|
||||
SpeciesPicker.draw(S, Kit, width, height)
|
||||
@@ -918,6 +942,9 @@ function App.keypressed(key)
|
||||
if not S or S.missingCache then
|
||||
return
|
||||
end
|
||||
if TouchEditor.keypressed(S, Kit, key) then
|
||||
return
|
||||
end
|
||||
if Chooser.keypressed(S, key) then
|
||||
return
|
||||
end
|
||||
@@ -1028,7 +1055,7 @@ function App.wheelmoved(x, y)
|
||||
if not S or S.missingCache then
|
||||
return
|
||||
end
|
||||
if S.navPopup then
|
||||
if S.navPopup or S.editPopup or S.speciesPicker or S.movePicker or S.itemPicker then
|
||||
wheelY = wheelY + (y or 0)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -12,7 +12,8 @@ local function id(option)
|
||||
end
|
||||
|
||||
local function label(option)
|
||||
return option.label or option[2]
|
||||
local text = option.label or option[2]
|
||||
return (option.errors or 0) > 0 and (text .. " (" .. option.errors .. ")") or text
|
||||
end
|
||||
|
||||
function Chooser.close(S)
|
||||
@@ -64,6 +65,7 @@ function Chooser.navigation(S, kit, key, title, options, x, y, w, h, after)
|
||||
if
|
||||
kit.button(x, y, w, h, text, {
|
||||
id = "navigate-" .. key,
|
||||
invalid = (current.errors or 0) > 0,
|
||||
face = "invert",
|
||||
font = "small",
|
||||
icon = icon,
|
||||
@@ -202,6 +204,7 @@ function Chooser.draw(S, kit, width, height)
|
||||
if
|
||||
kit.button(cx, cy + (i - 1) * (row + gap) - popup.scroll, contentW, row, text, {
|
||||
face = "selection",
|
||||
invalid = (option.errors or 0) > 0,
|
||||
active = active(S, popup, option),
|
||||
font = "small",
|
||||
align = "left",
|
||||
|
||||
@@ -19,7 +19,7 @@ local function restore(S, snapshot)
|
||||
S.editingMon, S.formMon, S.nicknameMon = nil, nil, nil
|
||||
S.monDrafts, S.trainerDrafts, S.walletDrafts = {}, {}, {}
|
||||
S.propertyChoice, S.itemMenu = nil, nil
|
||||
S.navPopup = nil
|
||||
S.navPopup, S.editPopup = nil, nil
|
||||
S.speciesPicker, S.movePicker, S.itemPicker = nil, nil, nil
|
||||
S.revision = (S.revision or 0) + 1
|
||||
require("Gen").ensureBoxes(S.save)
|
||||
|
||||
+168
-116
@@ -6,8 +6,11 @@ local PAL = require("Theme").PAL
|
||||
local Body = {}
|
||||
local Motion = require("Motion")
|
||||
local Chooser = require("Chooser")
|
||||
local Touch = require("TouchEditor")
|
||||
local Limits = require("ValueLimits")
|
||||
local Named = require("NamedChoices")
|
||||
|
||||
local function drawSection(S, Kit, x, y, w, h)
|
||||
local function drawSection(S, Kit, x, y, w, h, report, issues)
|
||||
local mon = S.editingMon
|
||||
if not mon then
|
||||
Kit.textWrapped(
|
||||
@@ -32,6 +35,10 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
{ "extras", "Extras" },
|
||||
{ "checks", "Checks" },
|
||||
}
|
||||
for _, entry in ipairs(sections) do
|
||||
entry.errors = issues.sections[entry[1]]
|
||||
entry.icon = entry.errors > 0 and "triangle-alert" or Kit.navigationIcon(entry[2])
|
||||
end
|
||||
S.monSection = S.monSection or "main"
|
||||
local navY = y + pad
|
||||
local navX = cx
|
||||
@@ -71,16 +78,35 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
local function text(str, color)
|
||||
cy = cy + Kit.textWrapped("small", str, cx, cy, inner, color or PAL.caption) + gap
|
||||
end
|
||||
local function button(label, fn, kind)
|
||||
local opts = { kind = kind, font = "small" }
|
||||
local function hint(id)
|
||||
cy = cy + Touch.issue(Kit, issues.fields[id], cx, cy, inner)
|
||||
end
|
||||
if report.errors > 0 then
|
||||
text(report.errors .. " saved value errors. Check the red fields.", PAL.red)
|
||||
end
|
||||
local function button(label, fn, kind, help, id)
|
||||
local issue = id and issues.fields[id]
|
||||
local opts = { kind = kind, font = "small", invalid = issue ~= nil }
|
||||
local buttonH = Kit.buttonHeight(label, inner, opts)
|
||||
if Kit.button(cx, cy, inner, buttonH, label, opts) then
|
||||
if help then
|
||||
buttonH = Touch.action(S, Kit, label, help, fn, cx, cy, inner, kind, issue)
|
||||
elseif Kit.button(cx, cy, inner, buttonH, label, opts) then
|
||||
fn()
|
||||
end
|
||||
cy = cy + buttonH + gap
|
||||
if issue and not help then hint(id) end
|
||||
end
|
||||
local function field(id, label, value, fn, sanitize)
|
||||
text(label, PAL.text)
|
||||
if type(value) == "number" then
|
||||
label = label:gsub(" %(.-%)", "")
|
||||
cy = cy
|
||||
+ Touch.value(S, Kit, id, label, value, function()
|
||||
return Limits.mon(S, mon, id)
|
||||
end, cx, cy, inner, fn, issues.fields[id])
|
||||
+ 2 * gap
|
||||
return
|
||||
end
|
||||
text(label, issues.fields[id] and PAL.red or PAL.text)
|
||||
local key = "property-" .. id
|
||||
local draft = S.monDrafts[id]
|
||||
if Kit.focus ~= key and draft == nil then
|
||||
@@ -106,7 +132,7 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
row,
|
||||
draft or tostring(value or ""),
|
||||
"value",
|
||||
{ sanitize = sanitize, onSubmit = apply }
|
||||
{ sanitize = sanitize, onSubmit = apply, invalid = issues.fields[id] ~= nil }
|
||||
)
|
||||
if
|
||||
Kit.button(cx + inner - applyW, cy, applyW, row, "Set", { kind = "accent", font = "small" })
|
||||
@@ -115,65 +141,28 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
Kit.blur()
|
||||
end
|
||||
cy = cy + row + 2 * gap
|
||||
hint(id)
|
||||
end
|
||||
local function choice(id, label, value, options, apply)
|
||||
local shown = "Unknown (" .. tostring(value) .. ")"
|
||||
for _, v in ipairs(options) do
|
||||
if v[1] == value then
|
||||
shown = v[2]
|
||||
end
|
||||
end
|
||||
text(label, PAL.text)
|
||||
if
|
||||
Kit.button(cx, cy, inner, row, shown, {
|
||||
face = "invert",
|
||||
font = "small",
|
||||
trailingIcon = S.propertyChoice == id and "chevron-up" or "chevron-down",
|
||||
})
|
||||
then
|
||||
if S.propertyChoice == id then
|
||||
S.propertyChoice = nil
|
||||
else
|
||||
S.propertyChoice = id
|
||||
S._choiceScroll = S.inspectorScroll
|
||||
end
|
||||
Kit.blur()
|
||||
end
|
||||
cy = cy + row + gap
|
||||
if S.propertyChoice == id then
|
||||
local cw = (inner - gap) / 2
|
||||
for i, v in ipairs(options) do
|
||||
if
|
||||
Kit.chip(
|
||||
cx + (i - 1) % 2 * (cw + gap),
|
||||
cy + math.floor((i - 1) / 2) * (row + gap),
|
||||
cw,
|
||||
row,
|
||||
v[2],
|
||||
v[1] == value,
|
||||
PAL.blue
|
||||
)
|
||||
then
|
||||
apply(v[1])
|
||||
S.propertyChoice = nil
|
||||
S.inspectorScroll = math.min(S._choiceScroll or 0, S.inspectorScroll or 0)
|
||||
end
|
||||
end
|
||||
cy = cy + math.ceil(#options / 2) * (row + gap)
|
||||
end
|
||||
cy = cy + gap
|
||||
cy = cy + Touch.choice(S, Kit, id, label, value, options, cx, cy, inner, apply, nil, issues.fields[id]) + 2 * gap
|
||||
end
|
||||
local function props(list)
|
||||
for _, d in ipairs(list) do
|
||||
if d.toggle then
|
||||
local on = P.get(mon, d)
|
||||
on = on == true or on == 1
|
||||
if Kit.chip(cx, cy, inner, row, d.label .. ": " .. (on and "ON" or "OFF"), on) then
|
||||
local issue = issues.fields[d.key]
|
||||
local shown = issue and tostring(P.get(mon, d)) or (on and "ON" or "OFF")
|
||||
local label = d.label .. ": " .. shown
|
||||
local opts = { face = "selection", active = on, font = "small", invalid = issue ~= nil }
|
||||
local height = Kit.buttonHeight(label, inner, opts)
|
||||
if Kit.button(cx, cy, inner, height, label, opts) then
|
||||
Ops.setMonProperty(S, mon, d.key, not on)
|
||||
end
|
||||
cy = cy + row + gap
|
||||
elseif d.choices then
|
||||
choice(d.key, d.label, P.get(mon, d), d.choices, function(v)
|
||||
cy = cy + height + gap
|
||||
hint(d.key)
|
||||
elseif Named.property(S, d) then
|
||||
choice(d.key, d.label, P.get(mon, d), Named.property(S, d), function(v)
|
||||
return Ops.setMonProperty(S, mon, d.key, v)
|
||||
end)
|
||||
else
|
||||
@@ -188,7 +177,7 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
text(def.name or tostring(mon.species or mon.speciesId), PAL.heading)
|
||||
button("Change species", function()
|
||||
Ops.openSpeciesPicker(S, Kit)
|
||||
end, "accent")
|
||||
end, "accent", nil, "species")
|
||||
if S.nicknameMon ~= mon then
|
||||
S.nicknameMon, S.nicknameDraft = mon, mon.nickname or ""
|
||||
end
|
||||
@@ -203,6 +192,7 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
S.nicknameDraft or "",
|
||||
"no nickname",
|
||||
{
|
||||
invalid = issues.fields.nickname ~= nil,
|
||||
sanitize = function(v)
|
||||
return Ops.nicknameSanitize(S, v)
|
||||
end,
|
||||
@@ -213,6 +203,7 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
Kit.blur()
|
||||
end
|
||||
cy = cy + row + gap
|
||||
hint("nickname")
|
||||
button("Clear nickname", function()
|
||||
Ops.clearNickname(S, mon)
|
||||
S.nicknameDraft = ""
|
||||
@@ -235,22 +226,30 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
return Ops.setCurrentHp(S, mon, v)
|
||||
end
|
||||
)
|
||||
button("Status: " .. tostring(mon.status or "healthy"), function()
|
||||
local choices = g == 1 and { "", "SLP", "PSN", "BRN", "FRZ", "PAR" }
|
||||
or { "", "SLP", "PSN", "BRN", "FRZ", "PAR", "TOX" }
|
||||
local at = 1
|
||||
for i, v in ipairs(choices) do
|
||||
if v == (mon.status or "") then
|
||||
at = i
|
||||
end
|
||||
end
|
||||
local want = choices[at % #choices + 1]
|
||||
Ops.setMonStatus(S, mon, want ~= "" and want or nil)
|
||||
local statuses = {
|
||||
{ "healthy", "Healthy" },
|
||||
{ "SLP", "Asleep" },
|
||||
{ "PSN", "Poisoned" },
|
||||
{ "BRN", "Burned" },
|
||||
{ "FRZ", "Frozen" },
|
||||
{ "PAR", "Paralyzed" },
|
||||
}
|
||||
if g >= 2 then
|
||||
statuses[#statuses + 1] = { "TOX", "Badly poisoned" }
|
||||
end
|
||||
local savedStatus = mon.status or "healthy"
|
||||
if type(savedStatus) == "number" then
|
||||
savedStatus = ({ [0]="healthy",[8]="PSN",[16]="BRN",[32]="FRZ",[64]="PAR",[128]="TOX" })[savedStatus]
|
||||
or (savedStatus >= 1 and savedStatus <= 7 and "SLP") or savedStatus
|
||||
end
|
||||
choice("status", "Status", savedStatus, statuses, function(v)
|
||||
return Ops.setMonStatus(S, mon, v ~= "healthy" and v or nil)
|
||||
end)
|
||||
if g >= 2 then
|
||||
button("Held item: " .. tostring(mon.heldItem or mon.item or "none"), function()
|
||||
local held = S.data.items and S.data.items[mon.heldItem or mon.item]
|
||||
button("Held item: " .. tostring(held and held.name or "None"), function()
|
||||
Ops.openItemPicker(S, Kit, "held")
|
||||
end, "accent")
|
||||
end, "accent", nil, "heldItem")
|
||||
button("Clear held item", function()
|
||||
Ops.setHeldItem(S, mon, nil)
|
||||
end, "danger")
|
||||
@@ -265,7 +264,7 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
if g == 3 then
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local Summary = require("src.core.game3.summary_data")
|
||||
local nature = (mon.personality or 0) % 25
|
||||
local nature = mon.nature or (tonumber(mon.personality) or 0) % 25
|
||||
local natures = {}
|
||||
for id = 0, 24 do
|
||||
local ok, name = pcall(function()
|
||||
@@ -276,39 +275,57 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
choice("nature", "Nature", nature, natures, function(v)
|
||||
return Ops.setNature(S, mon, v)
|
||||
end)
|
||||
local slot = mon.abilityNum or (mon.personality or 0) % 2
|
||||
button(
|
||||
"Ability: "
|
||||
.. tostring(
|
||||
Pokemon.abilityName(mon.ability or Pokemon.abilityId(mon.species, mon.personality))
|
||||
)
|
||||
.. " / slot "
|
||||
.. (slot + 1),
|
||||
function()
|
||||
Ops.setAbility(S, mon, 1 - slot)
|
||||
end,
|
||||
"accent"
|
||||
)
|
||||
local gender = Pokemon.gender(mon.species, mon.personality)
|
||||
button("Gender: " .. gender, function()
|
||||
Ops.setMonGender(S, mon, gender == "M" and "F" or "M")
|
||||
end)
|
||||
button("Shiny: " .. (Pokemon.isShiny(mon) and "ON" or "OFF"), function()
|
||||
Ops.setShiny(S, mon, not Pokemon.isShiny(mon))
|
||||
end, "warn")
|
||||
text(
|
||||
"Nature, ability, gender and shiny changes regenerate PID. Use Checks to review the result."
|
||||
)
|
||||
elseif g == 2 then
|
||||
field("pokerus", "Pokerus (packed strain / days)", mon.pokerus or 0, function(v)
|
||||
local n = tonumber(v)
|
||||
if not require("Legality").integer(n, 0, 255) then
|
||||
return Ops.say(S, "Pokerus must be 0-255")
|
||||
local slot = mon.abilityNum or (tonumber(mon.personality) or 0) % 2
|
||||
local abilities = {}
|
||||
for i, id in ipairs(Pokemon.abilities(mon.species)) do
|
||||
if id and id ~= 0 then
|
||||
abilities[#abilities + 1] = { i - 1, Pokemon.abilityName(id) }
|
||||
end
|
||||
return Ops.setPokerus(S, mon, n)
|
||||
end
|
||||
choice("ability", "Ability", slot, abilities, function(v)
|
||||
return Ops.setAbility(S, mon, v)
|
||||
end)
|
||||
local gender = mon.gender or Pokemon.gender(mon.species, tonumber(mon.personality) or 0)
|
||||
local ratio = (Pokemon.speciesMeta(mon.species) or {}).genderRatio or 255
|
||||
local genders = ratio == 255 and { { "U", "Genderless" } }
|
||||
or ratio == 0 and { { "M", "Male" } }
|
||||
or ratio == 254 and { { "F", "Female" } }
|
||||
or { { "M", "Male" }, { "F", "Female" } }
|
||||
choice("gender", "Gender", gender, genders, function(v)
|
||||
return Ops.setMonGender(S, mon, v)
|
||||
end)
|
||||
local shiny = mon.isShiny
|
||||
if shiny == nil then shiny = Pokemon.isShiny(mon) end
|
||||
button("Shiny: " .. (shiny and "ON" or "OFF"), function()
|
||||
Ops.setShiny(S, mon, not shiny)
|
||||
end, "warn", "Changes shininess and personality. Review Checks after editing.", "shiny")
|
||||
elseif g == 2 then
|
||||
local d = P.find(S, "pokerus")
|
||||
choice("pokerus", "Pokérus", mon.pokerus or 0, Named.property(S, d), function(v)
|
||||
return Ops.setPokerus(S, mon, v)
|
||||
end)
|
||||
text("Gender, shininess and Unown form follow DVs.")
|
||||
for _, id in ipairs({ "gender", "shiny", "form" }) do hint(id) end
|
||||
end
|
||||
button("Fix all errors", function()
|
||||
Ops.fixMonErrors(S, mon)
|
||||
end, "good", "Fixes invalid values, stats and PP. Origin warnings still need checking.")
|
||||
button(
|
||||
"Randomize Pokémon",
|
||||
function()
|
||||
Ops.randomizeMon(S, mon)
|
||||
end,
|
||||
"accent",
|
||||
"Replaces this Pokémon with a wild one from this game. Real level range, normal moves. Undo brings yours back."
|
||||
)
|
||||
button(
|
||||
"Max out Pokémon",
|
||||
function()
|
||||
Ops.maxMon(S, mon)
|
||||
end,
|
||||
"good",
|
||||
"Level 100, max IVs or DVs, friendship and PP. Spare EVs go to the strongest stats. Fully heals."
|
||||
)
|
||||
button("Full heal", function()
|
||||
Ops.healMon(S, mon)
|
||||
end, "good")
|
||||
@@ -326,7 +343,7 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
tostring(stats.defense or mon.defense or "?"),
|
||||
tostring(stats.speed or mon.speed or "?")
|
||||
),
|
||||
PAL.heading
|
||||
issues.fields.calculated and PAL.red or PAL.heading
|
||||
)
|
||||
text(
|
||||
g == 1 and ("Special: " .. tostring(stats.special or "?"))
|
||||
@@ -336,16 +353,17 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
.. " / Sp. Def: "
|
||||
.. tostring(stats.specialDefense or stats.spDef or mon.spDef or "?")
|
||||
),
|
||||
PAL.heading
|
||||
issues.fields.calculated and PAL.red or PAL.heading
|
||||
)
|
||||
hint("calculated")
|
||||
text(
|
||||
g == 3 and "IVs 0-31. EVs 0-255 with a total limit of 510."
|
||||
or "DVs 0-15. HP DV follows the other DVs. Stat experience 0-65535."
|
||||
)
|
||||
if g == 3 then
|
||||
button("Max all (31)", function()
|
||||
button("Max all IVs", function()
|
||||
Ops.maxIvs(S, mon)
|
||||
end, "good")
|
||||
end, "good", "Sets all six IVs to 31. Origin checks may still need review.")
|
||||
button("Clear EVs", function()
|
||||
Ops.clearEvs(S, mon)
|
||||
end, "danger")
|
||||
@@ -353,16 +371,42 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
for _, k in ipairs(keys) do
|
||||
total = total + (mon.evs and mon.evs[k] or 0)
|
||||
end
|
||||
text("Total EVs: " .. total .. " / 510", total > 510 and PAL.red or PAL.green)
|
||||
text(
|
||||
"EVs: " .. total .. " / 510 · " .. math.max(0, 510 - total) .. " free",
|
||||
total > 510 and PAL.red or PAL.green
|
||||
)
|
||||
else
|
||||
button("Max all DVs", function()
|
||||
Ops.maxDvs(S, mon)
|
||||
end, "good", "Sets DVs to 15. In Gen 2 this can change gender and shininess.")
|
||||
button("Max stat training", function()
|
||||
Ops.maxStatExp(S, mon)
|
||||
end, "good", "Fills stat experience for every stat.")
|
||||
text("HP DV: " .. tostring(mon.dvs and mon.dvs.hp or 0) .. " / 15 · follows the other DVs", issues.fields["dv-hp"] and PAL.red)
|
||||
hint("dv-hp")
|
||||
end
|
||||
for _, k in ipairs(keys) do
|
||||
if g == 3 then
|
||||
field("iv-" .. k, k:upper() .. " IV", mon.ivs and mon.ivs[k] or 0, function(v)
|
||||
return Ops.setIv(S, mon, k, tonumber(v) or 0)
|
||||
end)
|
||||
field("ev-" .. k, k:upper() .. " EV", mon.evs and mon.evs[k] or 0, function(v)
|
||||
return Ops.setEv(S, mon, k, tonumber(v) or 0)
|
||||
end)
|
||||
field(
|
||||
"iv-" .. k,
|
||||
(
|
||||
{ hp = "HP", atk = "Attack", def = "Defense", spa = "Sp. Atk", spd = "Sp. Def", spe = "Speed" }
|
||||
)[k] .. " IV",
|
||||
mon.ivs and mon.ivs[k] or 0,
|
||||
function(v)
|
||||
return Ops.setIv(S, mon, k, tonumber(v) or 0)
|
||||
end
|
||||
)
|
||||
field(
|
||||
"ev-" .. k,
|
||||
(
|
||||
{ hp = "HP", atk = "Attack", def = "Defense", spa = "Sp. Atk", spd = "Sp. Def", spe = "Speed" }
|
||||
)[k] .. " EV",
|
||||
mon.evs and mon.evs[k] or 0,
|
||||
function(v)
|
||||
return Ops.setEv(S, mon, k, tonumber(v) or 0)
|
||||
end
|
||||
)
|
||||
else
|
||||
if k ~= "hp" then
|
||||
field("dv-" .. k, k:upper() .. " DV", mon.dvs and mon.dvs[k] or 0, function(v)
|
||||
@@ -386,7 +430,7 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
local md = id and S.data.moves and S.data.moves[id]
|
||||
button("Slot " .. slot .. ": " .. tostring(md and md.name or id or "empty"), function()
|
||||
Ops.openMovePicker(S, Kit, slot)
|
||||
end, "accent")
|
||||
end, "accent", nil, "move" .. slot)
|
||||
if id and id ~= 0 then
|
||||
local pp = type(mv) == "table" and mv.pp or mon.pp and mon.pp[slot] or 0
|
||||
local ups = MonOps.getPpUps(mon, slot)
|
||||
@@ -417,18 +461,22 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
end
|
||||
elseif S.monSection == "extras" then
|
||||
if g == 3 then
|
||||
text("Contest conditions")
|
||||
text("Contest conditions", issues.fields.contest and PAL.red)
|
||||
hint("contest")
|
||||
props(P.contest)
|
||||
text("Ribbons: setting a ribbon does not establish award or event eligibility.")
|
||||
text("Ribbons: setting a ribbon does not establish award or event eligibility.", issues.fields.ribbons and PAL.red)
|
||||
hint("ribbons")
|
||||
props(P.ribbons)
|
||||
else
|
||||
text("This generation has no contest conditions or ribbons.")
|
||||
end
|
||||
else
|
||||
local report = require("Legality").mon(S, mon)
|
||||
button("Fix all errors", function()
|
||||
Ops.fixMonErrors(S, mon)
|
||||
end, "good", "Fixes invalid values, stats and PP. Does not invent encounter or event history.")
|
||||
text(
|
||||
report.errors > 0 and (report.errors .. " property errors")
|
||||
or "Property checks passed; encounter legality is unchecked",
|
||||
or "Values pass. Origin still needs checking.",
|
||||
report.errors > 0 and PAL.red or PAL.yellow
|
||||
)
|
||||
for _, check in ipairs(report.checks) do
|
||||
@@ -443,6 +491,10 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
Kit.scrollbar(cx, bodyY, inner, bodyH, S.inspectorScroll, S._formHeight, bodyH)
|
||||
end
|
||||
function Body.draw(S, Kit, x, y, w, h)
|
||||
Motion.pages(S, Kit, "monSection", x, y, w, h, drawSection)
|
||||
local report = require("Legality").mon(S, S.editingMon)
|
||||
local issues = require("Legality").highlights(report, S.editingMon)
|
||||
Motion.pages(S, Kit, "monSection", x, y, w, h, function(state, kit, px, py, pw, ph)
|
||||
drawSection(state, kit, px, py, pw, ph, report, issues)
|
||||
end)
|
||||
end
|
||||
return Body
|
||||
|
||||
+106
-22
@@ -73,19 +73,47 @@ function Kit.beginFrame(mx, my, clicked, wheel)
|
||||
Kit._fieldHit = false
|
||||
Kit._focusDrawn = false
|
||||
Kit.wheelY = wheel or 0
|
||||
-- Held-button state is polled, not evented: the editor is hosted both
|
||||
-- standalone and inside the launcher, and neither routes mousereleased
|
||||
-- here. Touch drag scrolling (#715) rides this poll, so it works in both
|
||||
-- hosts without new plumbing. The stub has no love.mouse.isDown; a frame
|
||||
-- without it simply has no drags.
|
||||
-- Mouse taps dispatch on release, so dragging never activates a row.
|
||||
-- The real touch stream supplies its own precise drag deltas. SDL
|
||||
-- synthesized mouse holds are suppressed until the finger is up.
|
||||
local down = false
|
||||
if love and love.mouse and love.mouse.isDown then
|
||||
down = love.mouse.isDown(1) and true or false
|
||||
end
|
||||
if Kit.ignoreMouseDown then
|
||||
if not down then
|
||||
Kit.ignoreMouseDown = nil
|
||||
end
|
||||
down = false
|
||||
end
|
||||
Kit.mouseDown = down
|
||||
if Kit.touchDown == true then
|
||||
Kit.mouseDown = true
|
||||
end
|
||||
if Kit.mouseDown and not Kit.touchDown then
|
||||
local d = Kit._pointerDrag
|
||||
if not d then
|
||||
Kit._pointerDrag = { x = mx, y = my, lastY = my, startY = my, moved = false }
|
||||
else
|
||||
if math.abs(my - d.startY) + math.abs(mx - d.x) > 10 then
|
||||
d.moved = true
|
||||
end
|
||||
if d.moved then
|
||||
Kit.dragAdd(d.lastY - my)
|
||||
end
|
||||
d.lastY = my
|
||||
end
|
||||
if clicked then
|
||||
Kit._tapPending = true
|
||||
end
|
||||
Kit.mouseClicked = false
|
||||
elseif Kit._pointerDrag then
|
||||
local d = Kit._pointerDrag
|
||||
if Kit._tapPending and not d.moved then
|
||||
Kit.mouseClicked = true
|
||||
end
|
||||
Kit._pointerDrag, Kit._tapPending = nil, nil
|
||||
end
|
||||
if not Kit.mouseDown then
|
||||
Kit._drag = nil
|
||||
end
|
||||
@@ -106,6 +134,10 @@ function Kit.endFrame()
|
||||
edits[i] = nil
|
||||
end
|
||||
Kit.wheelY = 0
|
||||
Kit._dragDelta = 0
|
||||
if not Kit.touchDown then
|
||||
Kit._touchDrag = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Use the launcher's font scale, with reflow and scrolling on small screens.
|
||||
@@ -443,8 +475,17 @@ local ACTION_ICONS = {
|
||||
Prev = "chevron-left",
|
||||
Next = "chevron-right",
|
||||
}
|
||||
local ERROR_FILL = { 58, 31, 37 }
|
||||
local function errorFace(opts)
|
||||
if not opts or not opts.invalid then return opts or {} end
|
||||
local copy = {}
|
||||
for key, value in pairs(opts) do copy[key] = value end
|
||||
copy.face, copy.fill, copy.ink, copy.stroke = "invert", ERROR_FILL, PAL.red, PAL.red
|
||||
copy.trailingIcon, copy.ring = copy.trailingIcon or "triangle-alert", false
|
||||
return copy
|
||||
end
|
||||
function Kit.buttonWidth(label, opts, h)
|
||||
opts = opts or {}
|
||||
opts = errorFace(opts)
|
||||
h = h or Kit.controlH()
|
||||
if opts.iconOnly then
|
||||
return h
|
||||
@@ -462,7 +503,7 @@ end
|
||||
|
||||
function Kit.buttonHeight(label, w, opts)
|
||||
local h = Kit.controlH()
|
||||
local height = Button.labelLayout(Kit, w, h, label, opts or {}).height + 4 * Kit.scale
|
||||
local height = Button.labelLayout(Kit, w, h, label, errorFace(opts)).height + 4 * Kit.scale
|
||||
return math.max(h, math.ceil(height))
|
||||
end
|
||||
|
||||
@@ -474,7 +515,7 @@ function Kit.button(x, y, w, h, label, opts)
|
||||
for key, value in pairs(opts or {}) do
|
||||
faceOpts[key] = value
|
||||
end
|
||||
opts = faceOpts
|
||||
opts = errorFace(faceOpts)
|
||||
opts.radius = Theme.radius()
|
||||
opts.segments = Theme.CONTROL.segments
|
||||
opts.emboss = false
|
||||
@@ -492,6 +533,9 @@ function Kit.button(x, y, w, h, label, opts)
|
||||
opts.labelLayout = Button.labelLayout(Kit, w, h, label, opts)
|
||||
end
|
||||
Button.draw(Kit, x, y, w, h, shown, opts, hot, false)
|
||||
if opts.invalid then
|
||||
Theme.stroke(x, y, w, h, Theme.radius(), PAL.red, 0.95, 2 * Kit.scale)
|
||||
end
|
||||
return opts.enabled ~= false and Kit.press(x, y, w, h) or false
|
||||
end
|
||||
|
||||
@@ -667,16 +711,17 @@ function Kit.textfield(id, x, y, w, h, value, placeholder, opts)
|
||||
end
|
||||
if G then
|
||||
local r = Theme.radius()
|
||||
Theme.fillRounded(x, y, w, h, PAL.rowBg, 0.7, r)
|
||||
local invalid = opts and opts.invalid
|
||||
Theme.fillRounded(x, y, w, h, invalid and ERROR_FILL or PAL.rowBg, 0.7, r)
|
||||
Theme.stroke(
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
r,
|
||||
focused and PAL.blue or PAL.cardBorder,
|
||||
focused and 0.8 or 0.3,
|
||||
focused and 1.5 * Kit.scale or 1
|
||||
invalid and PAL.red or focused and PAL.blue or PAL.cardBorder,
|
||||
invalid and 0.95 or focused and 0.8 or 0.3,
|
||||
invalid and 2 * Kit.scale or focused and 1.5 * Kit.scale or 1
|
||||
)
|
||||
local pad = 10 * Kit.scale
|
||||
local ty = y + (h - Kit.textHeight("mono")) / 2
|
||||
@@ -684,7 +729,7 @@ function Kit.textfield(id, x, y, w, h, value, placeholder, opts)
|
||||
Kit.text("mono", placeholder or "", x + pad, ty, PAL.faint)
|
||||
else
|
||||
local shown = Theme.ellipsizeLeft(font("mono"), value, w - 2 * pad)
|
||||
local tw = Kit.text("mono", shown, x + pad, ty, PAL.heading)
|
||||
local tw = Kit.text("mono", shown, x + pad, ty, invalid and PAL.red or PAL.heading)
|
||||
-- caret: blinks only while focused, parked at the end of the text
|
||||
if focused and (Kit.time % 1) < 0.55 then
|
||||
Theme.col(PAL.blue, 1)
|
||||
@@ -848,14 +893,19 @@ function Kit.scrollPixels(x, y, w, h, offset, contentH)
|
||||
return offset
|
||||
end
|
||||
|
||||
if Kit.mouseDown and maxOffset > 0 and h > 0 then
|
||||
local key = "px:" .. math.floor(x) .. ":" .. math.floor(y)
|
||||
local d = Kit._drag
|
||||
if not d and Kit.hit(x, y, w, h) then
|
||||
Kit._drag = { key = key, startY = Kit.mouseY, base = offset }
|
||||
elseif d and d.key == key then
|
||||
offset = Theme.clamp(d.base + (d.startY - Kit.mouseY), 0, maxOffset)
|
||||
end
|
||||
local d = Kit._pointerDrag or Kit._touchDrag
|
||||
if
|
||||
d
|
||||
and maxOffset > 0
|
||||
and h > 0
|
||||
and Kit._dragDelta ~= 0
|
||||
and d.x >= x
|
||||
and d.x <= x + w
|
||||
and d.startY >= y
|
||||
and d.startY <= y + h
|
||||
then
|
||||
offset = Theme.clamp(offset + (Kit._dragDelta or 0), 0, maxOffset)
|
||||
Kit._dragDelta = 0
|
||||
end
|
||||
|
||||
if (Kit.wheelY or 0) == 0 then
|
||||
@@ -865,11 +915,45 @@ function Kit.scrollPixels(x, y, w, h, offset, contentH)
|
||||
return offset
|
||||
end
|
||||
local notch = 48 * Kit.scale
|
||||
local delta = (Kit.wheelY > 0) and -notch or notch
|
||||
local delta = -Kit.wheelY * notch
|
||||
Kit.wheelY = 0
|
||||
return Theme.clamp(offset + delta, 0, maxOffset)
|
||||
end
|
||||
|
||||
function Kit.dragAdd(delta)
|
||||
Kit._dragDelta = (Kit._dragDelta or 0) + delta
|
||||
end
|
||||
|
||||
-- Lists keep the old row offset for keyboard/pager callers, but draw from a
|
||||
-- pixel offset so a finger moves the content by exactly the distance travelled.
|
||||
function Kit.list(S, key, x, y, w, h, total, stride, cols)
|
||||
cols = cols or 1
|
||||
S._listState = S._listState or {}
|
||||
local st = S._listState[key] or { pixels = (S[key] or 0) / cols * stride }
|
||||
S._listState[key] = st
|
||||
if st.first ~= nil and S[key] ~= st.first then
|
||||
st.pixels = (S[key] or 0) / cols * stride
|
||||
end
|
||||
if st.stride and (st.stride ~= stride or st.cols ~= cols) then
|
||||
st.pixels = (S[key] or 0) / cols * stride
|
||||
end
|
||||
st.stride, st.cols = stride, cols
|
||||
local content = math.max(0, math.ceil(total / cols) * stride)
|
||||
st.pixels = Kit.scrollPixels(x, y, w, h, st.pixels, content)
|
||||
local firstRow = math.floor(st.pixels / stride)
|
||||
local shift = st.pixels - firstRow * stride
|
||||
S[key], st.first = firstRow * cols, firstRow * cols
|
||||
st.content, st.view = content, h
|
||||
return math.min(total - S[key], math.ceil((h + shift) / stride) * cols), shift
|
||||
end
|
||||
|
||||
function Kit.listScrollbar(S, key, x, y, w, h)
|
||||
local st = S._listState and S._listState[key]
|
||||
if st then
|
||||
Kit.scrollbar(x, y, w, h, st.pixels, st.content, st.view)
|
||||
end
|
||||
end
|
||||
|
||||
-- Thin scrollbar in the card's right padding, clear of control faces and
|
||||
-- their corner arcs. Pure indicator (the drag above and the
|
||||
-- pager are the controls): on a phone the old layout looked "stuck" because
|
||||
|
||||
@@ -58,6 +58,10 @@ function L.mon(S, mon)
|
||||
add("error", "species", "Species is missing from the active game catalog")
|
||||
end
|
||||
range("level", mon.level, 1, 100)
|
||||
if type(mon.hp) ~= "number" or mon.hp ~= mon.hp or mon.hp ~= math.floor(mon.hp)
|
||||
or mon.hp < 0 or mon.hp == math.huge then
|
||||
add("error", "current HP", "Current HP must be a nonnegative whole number")
|
||||
end
|
||||
for _, key in ipairs({ "happiness", "friendship" }) do
|
||||
if mon[key] ~= nil then
|
||||
range(key, mon[key], 0, 255)
|
||||
@@ -388,6 +392,113 @@ function L.mon(S, mon)
|
||||
end
|
||||
return r
|
||||
end
|
||||
-- Match validator fields to the controls that edit them. Warnings are not errors.
|
||||
function L.highlights(report, mon)
|
||||
local out =
|
||||
{ fields = {}, sections = { main = 0, stats = 0, moves = 0, origin = 0, extras = 0, checks = report.errors } }
|
||||
local main = {
|
||||
species = true,
|
||||
nickname = true,
|
||||
level = true,
|
||||
experience = true,
|
||||
["current-hp"] = true,
|
||||
friendship = true,
|
||||
status = true,
|
||||
nature = true,
|
||||
gender = true,
|
||||
ability = true,
|
||||
shiny = true,
|
||||
heldItem = true,
|
||||
}
|
||||
local function section(id)
|
||||
if main[id] then
|
||||
return "main"
|
||||
end
|
||||
if id:match("^iv%-") or id:match("^ev%-") or id:match("^dv%-") or id:match("^se%-") or id == "calculated" then
|
||||
return "stats"
|
||||
end
|
||||
if id:match("^move%d") or id:match("^pp%-") or id:match("^ppup%-") then
|
||||
return "moves"
|
||||
end
|
||||
if id:match("^contest%.") or id:match("^ribbon%.") then
|
||||
return "extras"
|
||||
end
|
||||
return "origin"
|
||||
end
|
||||
for _, check in ipairs(report.checks) do
|
||||
if check.kind == "error" then
|
||||
local ids, f = {}, check.field
|
||||
local function add(id)
|
||||
ids[#ids + 1] = id
|
||||
end
|
||||
local key = f:match("^IV (.+)$")
|
||||
or f:match("^EV (.+)$")
|
||||
or f:match("^DV (.+)$")
|
||||
or f:match("^Stat experience (.+)$")
|
||||
if key then
|
||||
add((f:match("^IV ") and "iv-" or f:match("^EV ") and "ev-" or f:match("^DV ") and "dv-" or "se-") .. key)
|
||||
elseif f == "evs" or f == "ivs" then
|
||||
for _, k in ipairs(IV_KEYS) do
|
||||
add((f == "evs" and "ev-" or "iv-") .. k)
|
||||
end
|
||||
elseif f == "dvs" or f == "statExp" then
|
||||
for _, k in ipairs(DV_KEYS) do
|
||||
add((f == "dvs" and "dv-" or "se-") .. k)
|
||||
end
|
||||
elseif f == "moves" or f == "pp" or f == "ppBonuses" or f == "packed PP Ups" then
|
||||
for i = 1, 4 do
|
||||
add("move" .. i)
|
||||
add("pp-" .. i)
|
||||
add("ppup-" .. i)
|
||||
end
|
||||
elseif f:match("^move%d$") then
|
||||
local slot = f:match("(%d)$")
|
||||
add(f)
|
||||
if check.message:find("PP Ups", 1, true) then
|
||||
add("ppup-" .. slot)
|
||||
end
|
||||
if check.message:find("PP", 1, true) and not check.message:find("Empty move slot has PP Ups", 1, true) then
|
||||
add("pp-" .. slot)
|
||||
end
|
||||
elseif f:match("^PP %d$") then
|
||||
add("pp-" .. f:match("(%d)$"))
|
||||
elseif f:match("PP Ups %d$") then
|
||||
add("ppup-" .. f:match("(%d)$"))
|
||||
elseif f == "contest" then
|
||||
add("contest")
|
||||
for _, d in ipairs(Properties.contest) do
|
||||
if not mon or type(mon.contest) ~= "table" or (mon.contest[d.child] or 0) ~= 0 then
|
||||
add(d.key)
|
||||
end
|
||||
end
|
||||
elseif f == "ribbons" or f == "ribbon word" then
|
||||
add("ribbons")
|
||||
else
|
||||
add(
|
||||
({
|
||||
["current HP"] = "current-hp",
|
||||
happiness = "friendship",
|
||||
["ability slot"] = "ability",
|
||||
["dvs.hp"] = "dv-hp",
|
||||
sleep = "status",
|
||||
})[f]
|
||||
or (f:match("^stats") and "calculated")
|
||||
or f
|
||||
)
|
||||
end
|
||||
local counted = {}
|
||||
for _, id in ipairs(ids) do
|
||||
out.fields[id] = out.fields[id] or check.message
|
||||
local page = (id == "contest" or id == "ribbons") and "extras" or section(id)
|
||||
if not counted[page] then
|
||||
out.sections[page] = out.sections[page] + 1
|
||||
counted[page] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
function L.save(S)
|
||||
local report = { entries = {}, errors = 0, warnings = 0 }
|
||||
local function inspect(mon, label, box, slot)
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
local Copy = require("src.mods.Merge").deepCopy
|
||||
local Gen = require("Gen")
|
||||
local M = require("MonOps")
|
||||
local P = require("Properties")
|
||||
local Limits = require("ValueLimits")
|
||||
local A = {}
|
||||
local function clamp(v, lo, hi)
|
||||
v = tonumber(v)
|
||||
if not v or v ~= v or v == math.huge or v == -math.huge then
|
||||
v = lo
|
||||
end
|
||||
return math.max(lo, math.min(hi, math.floor(v)))
|
||||
end
|
||||
local function replace(mon, staged)
|
||||
for k in pairs(mon) do
|
||||
mon[k] = nil
|
||||
end
|
||||
for k, v in pairs(staged) do
|
||||
mon[k] = v
|
||||
end
|
||||
end
|
||||
local function cleanName(S, name, max)
|
||||
local nameClean = require("Ops").nicknameSanitize(S, tostring(name or ""))
|
||||
local chars = {}
|
||||
for ch in nameClean:gmatch("[%z\1-\127\194-\244][\128-\191]*") do
|
||||
if #chars < max then
|
||||
chars[#chars + 1] = ch
|
||||
end
|
||||
end
|
||||
return table.concat(chars)
|
||||
end
|
||||
function A.repair(S, original)
|
||||
local Ops, L = require("Ops"), require("Legality")
|
||||
if type(original) ~= "table" or not Ops.speciesUsable(S, original.species or original.speciesId) then
|
||||
return nil, "Choose a known species first"
|
||||
end
|
||||
local mon, g = Copy(original), Gen.ofState(S)
|
||||
local tables = g == 3 and { "ivs", "evs", "moves", "pp", "ppBonuses", "contest", "stats" }
|
||||
or { "dvs", "statExp", "moves", "pp", "ppBonuses", "stats" }
|
||||
for _, key in ipairs(tables) do
|
||||
if type(mon[key]) ~= "table" then
|
||||
mon[key] = {}
|
||||
end
|
||||
end
|
||||
mon.level = clamp(mon.level, 1, 100)
|
||||
mon.nickname = cleanName(S, mon.nickname, 10)
|
||||
for _, d in ipairs(P.all(S)) do
|
||||
local v = P.get(mon, d)
|
||||
if d.text then
|
||||
v = cleanName(S, v, d.max)
|
||||
elseif d.toggle then
|
||||
v = v == true or v == 1
|
||||
else
|
||||
v = clamp(v, d.lo, d.hi)
|
||||
if d.values and P.parse(d, v) == nil then
|
||||
v = d.default or d.choices[1][1]
|
||||
end
|
||||
end
|
||||
P.write(mon, d, v)
|
||||
end
|
||||
if mon.happiness ~= nil then
|
||||
mon.happiness = clamp(mon.happiness, 0, 255)
|
||||
end
|
||||
if mon.friendship ~= nil then
|
||||
mon.friendship = clamp(mon.friendship, 0, 255)
|
||||
end
|
||||
if g == 3 then
|
||||
local remaining = 510
|
||||
for _, k in ipairs(Limits.EV_KEYS) do
|
||||
mon.ivs[k] = clamp(mon.ivs[k], 0, 31)
|
||||
mon.evs[k] = clamp(mon.evs[k], 0, math.min(255, remaining))
|
||||
remaining = remaining - mon.evs[k]
|
||||
end
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
local pair = Pokemon.abilities(mon.species or mon.speciesId)
|
||||
mon.abilityNum = pair[2] and pair[2] ~= 0 and mon.personality % 2 or 0
|
||||
mon.ability, mon.abilityId, mon.isShiny = pair[mon.abilityNum + 1], pair[mon.abilityNum + 1], nil
|
||||
mon.metLevel = math.min(mon.metLevel or 0, mon.level)
|
||||
if mon.egg or mon.isEgg then
|
||||
mon.level, mon.metLevel, mon.language, mon.pokeball = 5, 0, 1, 4
|
||||
for _, d in ipairs(P.contest) do
|
||||
P.write(mon, d, 0)
|
||||
end
|
||||
end
|
||||
local bit = require("bit")
|
||||
local word = P.get(mon, { ribbon = true, shift = 0, width = 32 })
|
||||
word = bit.band(clamp(word, 0, 4294967295), 0x87FFFFFF)
|
||||
mon.ribbons = word < 0 and word + 4294967296 or word
|
||||
else
|
||||
for _, k in ipairs({ "attack", "defense", "speed", "special" }) do
|
||||
mon.dvs[k] = clamp(mon.dvs[k], 0, 15)
|
||||
end
|
||||
M.syncHpDv(mon.dvs)
|
||||
for _, k in ipairs({ "hp", "attack", "defense", "speed", "special" }) do
|
||||
mon.statExp[k] = clamp(mon.statExp[k], 0, 65535)
|
||||
end
|
||||
if g == 2 then
|
||||
M.setDv(S.data, mon, "attack", mon.dvs.attack, g)
|
||||
end
|
||||
end
|
||||
local strain = math.floor(clamp(mon.pokerus, 0, 255) / 16)
|
||||
if g == 2 then
|
||||
strain = math.min(8, strain)
|
||||
end
|
||||
local days = strain == 0 and 0 or math.min(clamp(mon.pokerus, 0, 255) % 16, strain % 4 + 1)
|
||||
if mon.pokerus ~= nil then
|
||||
mon.pokerus = strain * 16 + days
|
||||
end
|
||||
local held = mon.heldItem or mon.item
|
||||
if held and held ~= 0 and held ~= "NONE" and (g == 1 or not Ops.itemHoldable(S, held)) then
|
||||
mon.heldItem, mon.item = nil, nil
|
||||
end
|
||||
local status = { SLP = true, PSN = true, BRN = true, FRZ = true, PAR = true, TOX = g ~= 1 }
|
||||
if type(mon.status) == "number" then
|
||||
local allowed = { [0] = true, [8] = true, [16] = true, [32] = true, [64] = true, [128] = true }
|
||||
if not allowed[mon.status] and not (mon.status >= 1 and mon.status <= 7) then
|
||||
mon.status = nil
|
||||
end
|
||||
elseif not status[mon.status] then
|
||||
mon.status = nil
|
||||
end
|
||||
mon.sleep = mon.status == "SLP" and clamp(mon.sleep, 1, 7) or nil
|
||||
local seen, moves = {}, {}
|
||||
for slot = 1, 4 do
|
||||
local mv = mon.moves[slot]
|
||||
local id = type(mv) == "table" and (mv.moveId or mv.id) or mv
|
||||
if id and id ~= 0 and Ops.moveUsable(S, id) and not seen[id] then
|
||||
moves[slot], seen[id] = mv, true
|
||||
end
|
||||
end
|
||||
mon.moves = moves
|
||||
mon.ppBonusesPacked = clamp(mon.ppBonusesPacked, 0, 255)
|
||||
if next(moves) == nil and not (mon.egg or mon.isEgg) then
|
||||
local temp = { data = S.data, save = S.save, version = S.version }
|
||||
Ops.resetMoves(temp, mon)
|
||||
end
|
||||
for slot = 1, 4 do
|
||||
local mv = mon.moves[slot]
|
||||
if mv then
|
||||
local ups = clamp(M.getPpUps(mon, slot), 0, 3)
|
||||
if mon.egg or mon.isEgg or M.getBasePp(S.data, mon, slot) == 1 then
|
||||
ups = 0
|
||||
end
|
||||
M.setPpUps(S.data, mon, slot, ups, g)
|
||||
M.setPp(S.data, mon, slot, clamp(type(mv) == "table" and mv.pp or mon.pp[slot], 0, 255), g)
|
||||
else
|
||||
M.clearMove(mon, slot)
|
||||
mon.pp[slot] = 0
|
||||
M.setPpUps(S.data, mon, slot, 0, g)
|
||||
end
|
||||
end
|
||||
local lo = Limits.expAt(S, mon, mon.level)
|
||||
local hi = mon.level == 100 and lo or Limits.expAt(S, mon, mon.level + 1) - 1
|
||||
local exp = clamp(Gen.exp(mon), lo, hi)
|
||||
if g == 2 then
|
||||
mon.experience = exp
|
||||
else
|
||||
mon.exp = exp
|
||||
end
|
||||
if mon.exp ~= nil then
|
||||
mon.exp = exp
|
||||
end
|
||||
if mon.experience ~= nil then
|
||||
mon.experience = exp
|
||||
end
|
||||
M.recalc(S.data, mon, g)
|
||||
mon.hp = clamp(mon.hp, 0, mon.maxHp or mon.stats.hp)
|
||||
local report = L.mon(S, mon)
|
||||
if report.errors > 0 then
|
||||
for _, check in ipairs(report.checks) do
|
||||
if check.kind == "error" then
|
||||
return nil, check.message
|
||||
end
|
||||
end
|
||||
end
|
||||
return mon
|
||||
end
|
||||
function A.fixMon(S, mon)
|
||||
local Ops, L = require("Ops"), require("Legality")
|
||||
if L.mon(S, mon).errors == 0 then
|
||||
return Ops.say(S, "No property errors to fix")
|
||||
end
|
||||
local staged, err = A.repair(S, mon)
|
||||
if not staged then
|
||||
return Ops.say(S, err)
|
||||
end
|
||||
replace(mon, staged)
|
||||
return Ops.mark(S, "Fixed property errors. Check origin warnings separately.")
|
||||
end
|
||||
function A.fixAll(S)
|
||||
local fixed, left = 0, 0
|
||||
for _, entry in ipairs(require("Legality").save(S).entries) do
|
||||
if entry.report.errors > 0 then
|
||||
local staged = A.repair(S, entry.mon)
|
||||
if staged then
|
||||
replace(entry.mon, staged)
|
||||
fixed = fixed + 1
|
||||
else
|
||||
left = left + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local msg = "Fixed " .. fixed .. " Pokémon"
|
||||
if left > 0 then
|
||||
msg = msg .. "; " .. left .. " need a manual choice"
|
||||
end
|
||||
if fixed == 0 then
|
||||
return require("Ops").say(S, left > 0 and msg or "No property errors to fix")
|
||||
end
|
||||
return require("Ops").mark(S, msg .. ". Origin warnings stay for review.")
|
||||
end
|
||||
function A.maxMon(S, mon)
|
||||
local Ops, g = require("Ops"), Gen.ofState(S)
|
||||
local staged, err = A.repair(S, mon)
|
||||
if not staged then
|
||||
return Ops.say(S, err)
|
||||
end
|
||||
if staged.egg or staged.isEgg then
|
||||
return Ops.say(S, "Hatch the egg before maxing it out")
|
||||
end
|
||||
M.setLevel(S.data, staged, 100, g)
|
||||
M.setHappiness(S.data, staged, 255, g)
|
||||
if g == 3 then
|
||||
M.maxIvs(S.data, staged, g)
|
||||
-- Keep the existing spread; spend unused points on the strongest stats.
|
||||
local stats = staged.stats or {}
|
||||
local map = {
|
||||
hp = "hp",
|
||||
atk = "attack",
|
||||
def = "defense",
|
||||
spa = "specialAttack",
|
||||
spd = "specialDefense",
|
||||
spe = "speed",
|
||||
}
|
||||
local keys = Copy(Limits.EV_KEYS)
|
||||
table.sort(keys, function(a, b)
|
||||
local av, bv = stats[map[a]] or 0, stats[map[b]] or 0
|
||||
return av ~= bv and av > bv or av == bv and a < b
|
||||
end)
|
||||
for _, k in ipairs(keys) do
|
||||
M.setEv(S.data, staged, k, math.max(staged.evs[k] or 0, 252), g)
|
||||
end
|
||||
else
|
||||
for _, k in ipairs({ "attack", "defense", "speed", "special" }) do
|
||||
M.setDv(S.data, staged, k, 15, g)
|
||||
end
|
||||
for _, k in ipairs({ "hp", "attack", "defense", "speed", "special" }) do
|
||||
staged.statExp[k] = 65535
|
||||
end
|
||||
end
|
||||
M.maxAllPpUps(S.data, staged, g)
|
||||
M.recalc(S.data, staged, g)
|
||||
staged.hp, staged.status = staged.maxHp or staged.stats.hp, nil
|
||||
staged.sleep, staged.toxicCounter = nil, nil
|
||||
local result = require("Legality").mon(S, staged)
|
||||
if result.errors > 0 then
|
||||
for _, check in ipairs(result.checks) do
|
||||
if check.kind == "error" then
|
||||
return Ops.say(S, "Max refused: " .. check.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
replace(mon, staged)
|
||||
return Ops.mark(S, "Maxed level, stats, friendship and PP; fully healed")
|
||||
end
|
||||
-- pokefirered/src/wild_encounter.c:49 sUnownLetterSlots, by chamber.
|
||||
local UNOWN_LETTERS = {
|
||||
MAPSEC_MONEAN_CHAMBER = { 0, 27 },
|
||||
MAPSEC_LIPTOO_CHAMBER = { 2, 3, 7, 20, 14 },
|
||||
MAPSEC_WEEPTH_CHAMBER = { 13, 18, 8, 4 },
|
||||
MAPSEC_DILFORD_CHAMBER = { 15, 11, 9, 17, 16 },
|
||||
MAPSEC_SCUFIB_CHAMBER = { 24, 19, 6, 5, 10 },
|
||||
MAPSEC_RIXY_CHAMBER = { 21, 22, 23, 12, 1 },
|
||||
MAPSEC_VIAPOIS_CHAMBER = { 25, 26 },
|
||||
}
|
||||
local function random(lo, hi)
|
||||
return math.random(lo, hi)
|
||||
end
|
||||
function A.encounters(S)
|
||||
if S._randomEncounters then
|
||||
return S._randomEncounters
|
||||
end
|
||||
local candidates, g = {}, Gen.ofState(S)
|
||||
local tables = S.data.gen2Encounters or S.data.encounters or {}
|
||||
if g == 2 and (tables.grass or tables.water) then
|
||||
tables = { grass = tables.grass, water = tables.water }
|
||||
end
|
||||
if g == 3 then
|
||||
local E = require("src.core.game3.encounters")
|
||||
E.ensureLoaded()
|
||||
tables = {}
|
||||
for map in pairs(S.data.maps or {}) do
|
||||
if type(map) == "string" then
|
||||
tables[map] = E.tableFor(map)
|
||||
end
|
||||
end
|
||||
end
|
||||
local function walk(t, map, time)
|
||||
if type(t) ~= "table" then
|
||||
return
|
||||
end
|
||||
local sp = t.species or t.pokemon
|
||||
local lo = tonumber(t.minLevel or t.level)
|
||||
local hi = tonumber(t.maxLevel or t.level)
|
||||
if sp and lo and hi and require("Ops").speciesUsable(S, sp) then
|
||||
local def = map and S.data.maps and S.data.maps[map]
|
||||
local location = def and tonumber(g == 3 and def.regionMapSectionId or def.landmark)
|
||||
if (g ~= 3 and not Gen.hasCaughtData(S.save, S.version)) or location then
|
||||
candidates[#candidates + 1] =
|
||||
{ species = sp, lo = lo, hi = hi, map = map, location = location, time = time }
|
||||
end
|
||||
else
|
||||
for key, v in pairs(t) do
|
||||
local nextMap = type(key) == "string" and S.data.maps and S.data.maps[key] and key or map
|
||||
local nextTime = ({ MORN = 1, DAY = 2, NITE = 3 })[key] or time
|
||||
walk(v, nextMap, nextTime)
|
||||
end
|
||||
end
|
||||
end
|
||||
walk(tables)
|
||||
S._randomEncounters = candidates
|
||||
return candidates
|
||||
end
|
||||
function A.randomize(S, mon)
|
||||
local Ops, g = require("Ops"), Gen.ofState(S)
|
||||
local candidates = A.encounters(S)
|
||||
if #candidates == 0 then
|
||||
return Ops.say(S, "This game's encounter data is unavailable")
|
||||
end
|
||||
local row = candidates[random(1, #candidates)]
|
||||
local staged = M.create(S.data, row.species, random(row.lo, row.hi), g)
|
||||
staged.ot = S.save.player and S.save.player.name or S.save.name or "RED"
|
||||
staged.otName = staged.ot
|
||||
staged.otId = S.save.player and S.save.player.id or S.save.trainerId or 0
|
||||
if g == 3 then
|
||||
-- Independent stream: editing never advances the running game's RNG.
|
||||
local R = require("src.core.game3.rng")
|
||||
local seed = random(0, 65535) + random(0, 65535) * 65536
|
||||
local function next16()
|
||||
seed = (R.mulU32(seed, 1103515245) + 24691) % 4294967296
|
||||
return math.floor(seed / 65536)
|
||||
end
|
||||
local v = Gen.versionOf(S.save, S.version)
|
||||
local isRse = require("src.core.GameVersion").layout(v) == "rse"
|
||||
local constants = require("src.core.game3.constants").of(isRse and "emerald" or "firered")
|
||||
local sections = constants.region_map_sections.byName
|
||||
local safari = row.location == sections.MAPSEC_SAFARI_ZONE or row.location == sections.MAPSEC_KANTO_SAFARI_ZONE
|
||||
if safari then
|
||||
staged.pokeball = 5
|
||||
end
|
||||
if tonumber(staged.species) == constants:id("species", "SPECIES_UNOWN") and not isRse then
|
||||
local letters = UNOWN_LETTERS[constants:name("region_map_sections", row.location, "MAPSEC_")]
|
||||
if not letters then
|
||||
return Ops.say(S, "No verified Unown form for this location")
|
||||
end
|
||||
local letter = letters[random(1, #letters)]
|
||||
-- Unown uses high-half first; normal Random32 uses low-half first.
|
||||
repeat
|
||||
local high, low = next16(), next16()
|
||||
staged.personality = low + high * 65536
|
||||
until require("src.core.game3.pokemon").unownLetter(staged.personality) == letter
|
||||
else
|
||||
-- GenerateWildMon -> CreateMonWithNature -> CreateBoxMon. Preserve
|
||||
-- the native nature rejection loop and its following two IV draws.
|
||||
if isRse and safari then
|
||||
next16()
|
||||
end
|
||||
local nature = next16() % 25
|
||||
repeat
|
||||
local low, high = next16(), next16()
|
||||
staged.personality = low + high * 65536
|
||||
until staged.personality % 25 == nature
|
||||
end
|
||||
local iv1, iv2 = next16(), next16()
|
||||
staged.ivs = {
|
||||
hp = iv1 % 32,
|
||||
atk = math.floor(iv1 / 32) % 32,
|
||||
def = math.floor(iv1 / 1024) % 32,
|
||||
spe = iv2 % 32,
|
||||
spa = math.floor(iv2 / 32) % 32,
|
||||
spd = math.floor(iv2 / 1024) % 32,
|
||||
}
|
||||
staged.abilityNum = nil
|
||||
staged.otSecretId = S.save.secretId or 0
|
||||
staged.otGender = S.save.gender or 0
|
||||
staged.language, staged.metLocation, staged.metLevel = 2, row.location, staged.level
|
||||
staged.metGame = require("src.core.GameVersion").gameCode(Gen.versionOf(S.save, S.version))
|
||||
elseif g == 2 and Gen.hasCaughtData(S.save, S.version) then
|
||||
staged.caughtLevel = math.min(63, staged.level)
|
||||
staged.caughtLocation, staged.caughtTime = row.location, row.time or 2
|
||||
staged.caughtByGender = Gen.playerGender(S.save) == "female" and "girl" or "boy"
|
||||
end
|
||||
M.recalc(S.data, staged, g)
|
||||
staged.hp = staged.maxHp or staged.stats.hp
|
||||
local report = require("Legality").mon(S, staged)
|
||||
if report.errors > 0 then
|
||||
return Ops.say(S, "Randomization refused: generated values failed checks")
|
||||
end
|
||||
replace(mon, staged)
|
||||
return Ops.mark(S, "Randomized " .. Ops.monName(S, mon) .. " from this game's wild encounters")
|
||||
end
|
||||
return A
|
||||
@@ -0,0 +1,79 @@
|
||||
local Gen = require("Gen")
|
||||
local C = {}
|
||||
function C.locations(S)
|
||||
local rows = {}
|
||||
if Gen.ofState(S) == 3 then
|
||||
local pack = require("src.core.game3.encounters").loadCacheFile("region_map/map_sections.lua")
|
||||
local sections = pack and pack.sections
|
||||
local Maps = not sections and require("src.import.gba.map_sections_extract")
|
||||
for id = 0, 252 do
|
||||
local info = sections and sections[id]
|
||||
if not sections then
|
||||
local ok, result = pcall(Maps.getInfo, id, nil, 0)
|
||||
if ok and result and result.resolved then
|
||||
info = result
|
||||
end
|
||||
end
|
||||
if info and info.name and info.name ~= "" then
|
||||
local name = tostring(info.rawName or info.name):gsub("\n", " ")
|
||||
if info.id and info.id:find("POKECENTER", 1, true) then
|
||||
name = name .. " · Pokémon Center"
|
||||
end
|
||||
rows[#rows + 1] = { id, name }
|
||||
end
|
||||
end
|
||||
rows[#rows + 1] = { 253, "Special egg" }
|
||||
rows[#rows + 1] = { 254, "In-game trade" }
|
||||
rows[#rows + 1] = { 255, "Fateful encounter" }
|
||||
else
|
||||
rows = { { 0, "Unknown" } }
|
||||
for _, rec in pairs(S.data.gen2Landmarks and S.data.gen2Landmarks.landmarks or {}) do
|
||||
if type(rec) == "table" and rec.index and rec.index > 0 and rec.index < 126 then
|
||||
rows[#rows + 1] = { rec.index, tostring(rec.name or rec.id):gsub("\n", " ") }
|
||||
end
|
||||
end
|
||||
rows[#rows + 1], rows[#rows + 2] = { 127, "Event" }, { 126, "Gift" }
|
||||
end
|
||||
table.sort(rows, function(a, b)
|
||||
return a[2] < b[2]
|
||||
end)
|
||||
return rows
|
||||
end
|
||||
function C.property(S, d)
|
||||
if d.choices then
|
||||
return d.choices
|
||||
end
|
||||
if d.key == "metLocation" or d.key == "caughtLocation" then
|
||||
if not S._locationChoices then
|
||||
S._locationChoices = C.locations(S)
|
||||
end
|
||||
return S._locationChoices
|
||||
elseif d.key == "caughtTime" then
|
||||
return { { 0, "Unknown" }, { 1, "Morning" }, { 2, "Day" }, { 3, "Night" } }
|
||||
elseif d.key == "pokerus" then
|
||||
local rows = { { 0, "None" } }
|
||||
for strain = 1, Gen.ofState(S) == 2 and 8 or 15 do
|
||||
rows[#rows + 1] = { strain * 16, "Cured · strain " .. strain }
|
||||
for days = 1, strain % 4 + 1 do
|
||||
rows[#rows + 1] = {
|
||||
strain * 16 + days,
|
||||
"Strain " .. strain .. " · " .. days .. (days == 1 and " day left" or " days left"),
|
||||
}
|
||||
end
|
||||
end
|
||||
return rows
|
||||
elseif d.key == "markings" then
|
||||
local rows, names = {}, { "Circle", "Square", "Triangle", "Heart" }
|
||||
for mask = 0, 15 do
|
||||
local shown = {}
|
||||
for i, n in ipairs(names) do
|
||||
if math.floor(mask / 2 ^ (i - 1)) % 2 == 1 then
|
||||
shown[#shown + 1] = n
|
||||
end
|
||||
end
|
||||
rows[#rows + 1] = { mask, #shown == 0 and "None" or table.concat(shown, " + ") }
|
||||
end
|
||||
return rows
|
||||
end
|
||||
end
|
||||
return C
|
||||
@@ -2253,13 +2253,66 @@ function Ops.cloneMonToBox(S, mon)
|
||||
return Ops.say(S,"All boxes are full")
|
||||
end
|
||||
|
||||
function Ops.fixMonErrors(S, mon)
|
||||
local was = mon and (mon.item or mon.heldItem)
|
||||
local ok = require("MonActions").fixMon(S, mon)
|
||||
if ok then
|
||||
syncPartyMailHeldItem(S, mon, was, mon.item or mon.heldItem)
|
||||
end
|
||||
return ok
|
||||
end
|
||||
function Ops.fixAllErrors(S)
|
||||
local before = {}
|
||||
for i, mon in ipairs(S.save.party or {}) do
|
||||
before[i] = mon.item or mon.heldItem
|
||||
end
|
||||
local ok = require("MonActions").fixAll(S)
|
||||
if ok then
|
||||
for i, mon in ipairs(S.save.party or {}) do
|
||||
syncPartyMailHeldItem(S, mon, before[i], mon.item or mon.heldItem)
|
||||
end
|
||||
end
|
||||
return ok
|
||||
end
|
||||
function Ops.randomizeMon(S, mon)
|
||||
local was = mon and (mon.item or mon.heldItem)
|
||||
local ok = require("MonActions").randomize(S, mon)
|
||||
if ok then
|
||||
syncPartyMailHeldItem(S, mon, was, mon.item or mon.heldItem)
|
||||
syncPartyMailSpecies(S, mon)
|
||||
end
|
||||
return ok
|
||||
end
|
||||
function Ops.maxMon(S, mon)
|
||||
return require("MonActions").maxMon(S, mon)
|
||||
end
|
||||
function Ops.maxDvs(S, mon)
|
||||
for _, k in ipairs({ "attack", "defense", "speed", "special" }) do
|
||||
MonOps.setDv(S.data, mon, k, 15, Gen.ofState(S))
|
||||
end
|
||||
return Ops.mark(S, "Maxed DVs")
|
||||
end
|
||||
function Ops.maxStatExp(S, mon)
|
||||
for _, k in ipairs({ "hp", "attack", "defense", "speed", "special" }) do
|
||||
Ops.setStatExp(S, mon, k, 65535)
|
||||
end
|
||||
return true
|
||||
end
|
||||
function Ops.itemMax(S, id, pc)
|
||||
if not Ops.itemStacks(S, id) then
|
||||
return 1
|
||||
end
|
||||
return Gen.ofState(S) == 3 and g3Max(S, id, pc) or Ops.stackMax(S)
|
||||
end
|
||||
|
||||
-- Wrap only mutations. Nested helpers share one snapshot and one undo entry.
|
||||
for name, fn in pairs(Ops) do
|
||||
local mutation=type(fn)=="function" and (name:match("^set") or name:match("^clear")
|
||||
or name:match("^toggle") or name:match("^max") or name:match("^addTo")
|
||||
or name:match("^bag[A-Z]") or name:match("^pc[A-Z]") or name:match("^dex[A-Z]"))
|
||||
local extras={partyAdd=true,partyRemove=true,partyMove=true,boxAdd=true,boxAddSpecies=true,
|
||||
deposit=true,withdraw=true,release=true,healMon=true,resetMoves=true,cloneMonToBox=true,addMoney=true,addCoins=true}
|
||||
deposit=true,withdraw=true,release=true,healMon=true,resetMoves=true,cloneMonToBox=true,addMoney=true,addCoins=true,
|
||||
fixMonErrors=true,fixAllErrors=true,randomizeMon=true}
|
||||
local excluded={pcItems=true,pcOrder=true,pcCanMax=true,pcCanMaxAll=true,bagCanMax=true,
|
||||
bagCanMaxAll=true,dexCounts=true,dexList=true,dexSort=true,clearSelection=true}
|
||||
if (mutation or extras[name]) and not excluded[name] then
|
||||
|
||||
@@ -33,8 +33,8 @@ local G3 = {
|
||||
},
|
||||
}
|
||||
),
|
||||
field("metLevel", "Met level (0 hatched / 1-100 caught)", 0, 100),
|
||||
field("metLocation", "Met location ID", 0, 255),
|
||||
field("metLevel", "Found at level", 0, 100),
|
||||
field("metLocation", "Found at", 0, 255),
|
||||
field(
|
||||
"metGame",
|
||||
"Origin game",
|
||||
@@ -76,8 +76,8 @@ local G3 = {
|
||||
},
|
||||
}
|
||||
),
|
||||
field("pokerus", "Pokerus (packed strain / days)", 0, 255),
|
||||
field("markings", "Markings (bit mask)", 0, 15),
|
||||
field("pokerus", "Pokérus", 0, 255),
|
||||
field("markings", "Markings", 0, 15),
|
||||
field("isEgg", "Egg", nil, nil, { toggle = true, alias = "egg" }),
|
||||
field("modernFatefulEncounter", "Fateful encounter", nil, nil, { toggle = true }),
|
||||
}
|
||||
@@ -118,13 +118,13 @@ function P.identity(S)
|
||||
out[#out + 1] = d
|
||||
end
|
||||
elseif Gen.ofState(S) == 2 then
|
||||
out[#out + 1] = field("pokerus", "Pokerus (packed strain / days)", 0, 255)
|
||||
out[#out + 1] = field("pokerus", "Pokérus", 0, 255)
|
||||
out[#out + 1] = field("isEgg", "Egg", nil, nil, { toggle = true, alias = "egg" })
|
||||
if Gen.hasCaughtData(S.save, S.version) then
|
||||
out[#out + 1] = field("caughtLevel", "Caught level", 0, 63)
|
||||
out[#out + 1] = field("caughtLocation", "Caught landmark ID", 0, 127)
|
||||
out[#out + 1] = field("caughtLocation", "Found at", 0, 127)
|
||||
out[#out + 1] =
|
||||
field("caughtTime", "Caught time (0 unknown / 1 morning / 2 day / 3 night)", 0, 3)
|
||||
field("caughtTime", "Time found", 0, 3)
|
||||
end
|
||||
end
|
||||
return out
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
local Theme = require("Theme")
|
||||
local Chrome = require("PickerChrome")
|
||||
local PAL = Theme.PAL
|
||||
local E = {}
|
||||
local function limits(p)
|
||||
local l = type(p.limits) == "function" and p.limits() or p.limits
|
||||
return l or { lo = 0, hi = 255 }
|
||||
end
|
||||
function E.close(S, Kit)
|
||||
S.editPopup = nil
|
||||
Kit.blur()
|
||||
Kit.mouseClicked, Kit.wheelY, Kit._dragDelta = false, 0, 0
|
||||
end
|
||||
function E.open(S, Kit, p)
|
||||
Kit.blur()
|
||||
p.opened, p.scroll, p.query = true, 0, ""
|
||||
S.editPopup, Kit.blockClicks = p, true
|
||||
end
|
||||
function E.help(S, Kit, title, message, x, y)
|
||||
local tap, size = Kit.tapMin(), 22 * Kit.scale
|
||||
if Kit.press(x, y, tap, tap) then
|
||||
E.open(S, Kit, { mode = "help", title = title, help = message })
|
||||
end
|
||||
Theme.col(PAL.cardBorder, 0.8)
|
||||
love.graphics.circle("line", x + tap / 2, y + tap / 2, size / 2)
|
||||
Kit.textCenter("small", "?", x, y + (tap - Kit.textHeight("small")) / 2, tap, PAL.caption)
|
||||
end
|
||||
function E.issue(Kit, message, x, y, w)
|
||||
if not message then return 0 end
|
||||
local size, gap = 14 * Kit.scale, 6 * Kit.scale
|
||||
Kit.icon("triangle-alert", x, y + 2 * Kit.scale, size, PAL.red)
|
||||
return math.max(size, Kit.textWrapped("tiny", message, x + size + gap, y, w - size - gap, PAL.red)) + gap
|
||||
end
|
||||
function E.action(S, Kit, label, help, fn, x, y, w, kind, issue)
|
||||
local gap, tap = 8 * Kit.scale, Kit.tapMin()
|
||||
local opts = { kind = kind, font = "small", invalid = issue ~= nil }
|
||||
local h = Kit.buttonHeight(label, w - tap - gap, opts)
|
||||
if Kit.button(x, y, w - tap - gap, h, label, opts) then
|
||||
fn()
|
||||
end
|
||||
E.help(S, Kit, label, help, x + w - tap, y + (h - tap) / 2)
|
||||
return h + (issue and gap + E.issue(Kit, issue, x, y + h + gap, w) or 0)
|
||||
end
|
||||
function E.value(S, Kit, id, title, value, getLimits, x, y, w, apply, issue)
|
||||
local l = type(getLimits) == "function" and getLimits() or getLimits
|
||||
if not require("Legality").integer(value, l.lo, l.hi) then
|
||||
issue = issue or "Saved value must be a whole number from " .. l.lo .. " to " .. l.hi
|
||||
end
|
||||
local tap, gap = Kit.tapMin(), 8 * Kit.scale
|
||||
local opts = {
|
||||
font = "small",
|
||||
align = "left",
|
||||
trailingIcon = "pencil",
|
||||
face = "invert",
|
||||
id = "value-" .. id,
|
||||
invalid = issue ~= nil,
|
||||
}
|
||||
local label = title .. ": " .. tostring(value)
|
||||
local h = Kit.buttonHeight(label, w - tap - gap, opts)
|
||||
if Kit.button(x, y, w - tap - gap, h, label, opts) then
|
||||
E.open(S, Kit, { mode = "number", id = id, title = title, value = value, savedValue = value,
|
||||
issue = issue, limits = getLimits, apply = apply })
|
||||
end
|
||||
E.help(S, Kit, title, l.help, x + w - tap, y + (h - tap) / 2)
|
||||
local n = tonumber(value)
|
||||
local ratio = n and n == n and Theme.clamp((n - l.lo) / math.max(1, l.hi - l.lo), 0, 1) or 0
|
||||
local color = issue and PAL.red or PAL.blue
|
||||
local trackY, trackH = y + h + 6 * Kit.scale, 5 * Kit.scale
|
||||
Theme.fillRounded(x, trackY, w, trackH, PAL.cardBorder, 0.3, trackH / 2)
|
||||
if ratio > 0 then
|
||||
Theme.fillRounded(x, trackY, w * ratio, trackH, color, 0.9, trackH / 2)
|
||||
end
|
||||
local caption = l.lo .. " to " .. l.hi
|
||||
if l.remaining ~= nil then
|
||||
caption = caption .. " · " .. l.remaining .. " points free"
|
||||
end
|
||||
local ch = Kit.textWrapped("tiny", caption, x, trackY + trackH + 4 * Kit.scale, w, issue and PAL.red or PAL.caption)
|
||||
local height = h + 15 * Kit.scale + ch
|
||||
return height + (issue and gap + E.issue(Kit, issue, x, y + height + gap, w) or 0)
|
||||
end
|
||||
function E.choice(S, Kit, id, title, value, options, x, y, w, apply, help, issue)
|
||||
local shown = (issue and "Invalid saved value " or "Unknown saved value ") .. tostring(value)
|
||||
for _, o in ipairs(options) do
|
||||
if o[1] == value then
|
||||
shown = o[2]
|
||||
end
|
||||
end
|
||||
local opts = {
|
||||
face = "invert",
|
||||
font = "small",
|
||||
align = "left",
|
||||
trailingIcon = "chevron-down",
|
||||
id = "choice-" .. id,
|
||||
invalid = issue ~= nil,
|
||||
}
|
||||
local label = title .. ": " .. shown
|
||||
local h = Kit.buttonHeight(label, w, opts)
|
||||
if Kit.button(x, y, w, h, label, opts) then
|
||||
E.open(S, Kit, {
|
||||
mode = "choice",
|
||||
id = id,
|
||||
title = title,
|
||||
value = value,
|
||||
options = options,
|
||||
apply = apply,
|
||||
help = help,
|
||||
issue = issue,
|
||||
})
|
||||
end
|
||||
return h + (issue and 8 * Kit.scale + E.issue(Kit, issue, x, y + h + 8 * Kit.scale, w) or 0)
|
||||
end
|
||||
function E.results(p)
|
||||
local rows, query = {}, (p.query or ""):lower()
|
||||
for _, o in ipairs(p.options or {}) do
|
||||
if (o[2] .. " " .. tostring(o[1])):lower():find(query, 1, true) then
|
||||
rows[#rows + 1] = o
|
||||
end
|
||||
end
|
||||
return rows
|
||||
end
|
||||
function E.commit(S, Kit)
|
||||
local p = S.editPopup
|
||||
if not p then
|
||||
return false
|
||||
end
|
||||
local value = p.value
|
||||
if p.mode == "number" then
|
||||
local l = limits(p)
|
||||
if p.typing then
|
||||
value = tonumber(Kit.flushText("touch-exact", p.draft))
|
||||
end
|
||||
if not value or value ~= math.floor(value) or value < l.lo or value > l.hi then
|
||||
p.error = "Choose a whole number from " .. l.lo .. " to " .. l.hi
|
||||
return false
|
||||
end
|
||||
elseif p.mode == "choice" then
|
||||
local rows = E.results(p)
|
||||
local first = rows[p.index or 1] or rows[1]
|
||||
if not first then
|
||||
return false
|
||||
end
|
||||
value = first[1]
|
||||
end
|
||||
if p.apply then
|
||||
p.apply(value)
|
||||
end
|
||||
E.close(S, Kit)
|
||||
return true
|
||||
end
|
||||
function E.keypressed(S, Kit, key)
|
||||
local p = S.editPopup
|
||||
if not p then
|
||||
return false
|
||||
end
|
||||
if key == "escape" then
|
||||
E.close(S, Kit)
|
||||
elseif key == "return" or key == "kpenter" then
|
||||
if p.mode == "choice" then
|
||||
p.query = Kit.flushText("touch-search", p.query)
|
||||
end
|
||||
E.commit(S, Kit)
|
||||
elseif p.mode == "number" and not p.typing then
|
||||
local l = limits(p)
|
||||
local delta = ({ left = -1, right = 1, down = -5, up = 5 })[key]
|
||||
if delta then
|
||||
p.value = Theme.clamp(p.value + delta, l.lo, l.hi)
|
||||
end
|
||||
if key == "home" then
|
||||
p.value = l.lo
|
||||
elseif key == "end" then
|
||||
p.value = l.hi
|
||||
end
|
||||
elseif p.mode == "choice" and (key == "up" or key == "down" or key == "home" or key == "end") then
|
||||
local n = #E.results(p)
|
||||
if n > 0 then
|
||||
if key == "home" then
|
||||
p.index = 1
|
||||
elseif key == "end" then
|
||||
p.index = n
|
||||
else
|
||||
p.index = Theme.clamp((p.index or 1) + (key == "up" and -1 or 1), 1, n)
|
||||
end
|
||||
p.reveal, p.keyboard = true, true
|
||||
end
|
||||
else
|
||||
Kit.keypressed(key)
|
||||
end
|
||||
return true
|
||||
end
|
||||
function E.draw(S, Kit, width, height)
|
||||
local p = S.editPopup
|
||||
if not p then
|
||||
return
|
||||
end
|
||||
Kit.resetClip()
|
||||
Kit.blockClicks, p.opened = p.opened == true, nil
|
||||
local x, y, w, h, pad = Chrome.card(Kit, width, height)
|
||||
local row, gap, s = Kit.controlH(), 8 * Kit.scale, Kit.scale
|
||||
if p.mode == "help" then
|
||||
local oldH = h
|
||||
local _, lines = Kit.fonts.small:getWrap(p.help or "", w - 2 * pad)
|
||||
h = math.min(h, row + 2 * pad + gap + #lines * Kit.textHeight("small"))
|
||||
y = y + (oldH - h) / 2
|
||||
end
|
||||
p.rect = { x = x, y = y, w = w, h = h }
|
||||
Theme.col(PAL.bgBot, 0.78)
|
||||
love.graphics.rectangle("fill", 0, 0, width, height)
|
||||
if Kit.press(0, 0, width, height) and not Kit.hit(x, y, w, h) then
|
||||
E.close(S, Kit)
|
||||
return
|
||||
end
|
||||
Kit.card(x, y, w, h)
|
||||
local cx, inner, cy = x + pad, w - 2 * pad, y + pad
|
||||
Kit.textWrapped("small", p.title, cx, cy + 6 * s, inner - row - gap, PAL.heading)
|
||||
if Kit.iconButton(cx + inner - row, cy, row, row, "x", "Close") then
|
||||
E.close(S, Kit)
|
||||
return
|
||||
end
|
||||
cy = cy + row + gap
|
||||
if p.issue then
|
||||
local message = p.savedValue ~= nil and ("Saved: " .. tostring(p.savedValue) .. ". " .. p.issue) or p.issue
|
||||
cy = cy + E.issue(Kit, message, cx, cy, inner) + gap
|
||||
end
|
||||
if p.mode == "help" then
|
||||
Kit.textWrapped("small", p.help or "", cx, cy, inner, PAL.text)
|
||||
return
|
||||
end
|
||||
if p.mode == "choice" then
|
||||
p.query = Kit.textfield("touch-search", cx, cy, inner, row, p.query, "Search by name")
|
||||
cy = cy + row + gap
|
||||
if p.lastQuery ~= p.query then
|
||||
p.index, p.lastQuery, p.scroll = 1, p.query, 0
|
||||
end
|
||||
local bodyH, rows = y + h - pad - cy, E.results(p)
|
||||
local heights, total = {}, 0
|
||||
for i, o in ipairs(rows) do
|
||||
heights[i] = {
|
||||
top = total,
|
||||
h = Kit.buttonHeight(o[2], inner, { font = "small", align = "left", trailingIcon = "check" }),
|
||||
}
|
||||
total = total + heights[i].h + gap
|
||||
end
|
||||
if p.reveal and heights[p.index] then
|
||||
local selected = heights[p.index]
|
||||
p.scroll = Theme.clamp(
|
||||
math.max(selected.top + selected.h - bodyH, math.min(p.scroll, selected.top)),
|
||||
0,
|
||||
math.max(0, total - bodyH)
|
||||
)
|
||||
p.reveal = nil
|
||||
end
|
||||
p.scroll = Kit.scrollPixels(cx, cy, inner, bodyH, p.scroll, total)
|
||||
Kit.pushClip(cx, cy, inner, bodyH)
|
||||
for i, o in ipairs(rows) do
|
||||
local r = heights[i]
|
||||
if r.top + r.h >= p.scroll and r.top <= p.scroll + bodyH then
|
||||
if
|
||||
Kit.button(cx, cy + r.top - p.scroll, inner, r.h, o[2], {
|
||||
font = "small",
|
||||
align = "left",
|
||||
face = "selection",
|
||||
active = o[1] == p.value,
|
||||
trailingIcon = o[1] == p.value and "check" or nil,
|
||||
ring = p.keyboard and p.index == i or nil,
|
||||
})
|
||||
then
|
||||
p.apply(o[1])
|
||||
Kit.popClip()
|
||||
E.close(S, Kit)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
if #rows == 0 then
|
||||
Kit.textWrapped("small", "Nothing matches that.", cx, cy + gap, inner, PAL.caption)
|
||||
end
|
||||
Kit.popClip()
|
||||
Kit.scrollbar(cx, cy, inner, bodyH, p.scroll, total, bodyH)
|
||||
return
|
||||
end
|
||||
local l = limits(p)
|
||||
p.value = Theme.clamp(tonumber(p.value) or l.lo, l.lo, l.hi)
|
||||
local function setValue(value)
|
||||
p.value = Theme.clamp(value, l.lo, l.hi)
|
||||
p.typing, p.draft, p.error = nil, nil, nil
|
||||
Kit.blur()
|
||||
end
|
||||
local function spinWheel()
|
||||
p.wheelDelta = (p.wheelDelta or 0) + Kit.wheelY
|
||||
local delta = p.wheelDelta >= 0 and math.floor(p.wheelDelta) or math.ceil(p.wheelDelta)
|
||||
if delta ~= 0 then
|
||||
setValue(p.value + delta)
|
||||
p.wheelDelta = p.wheelDelta - delta
|
||||
end
|
||||
Kit.wheelY = 0
|
||||
end
|
||||
local helpH = Kit.textWrapped("tiny", l.help or "", cx, cy, inner, PAL.caption)
|
||||
cy = cy + helpH + gap
|
||||
local bodyY, bodyH = cy, math.max(0, y + h - pad - row - gap - cy)
|
||||
if p.roll and math.abs(Kit.mouseY - p.roll.y) > math.abs(Kit.mouseX - p.roll.x) + 8 * s then
|
||||
p.roll = nil
|
||||
p.verticalRoll = true
|
||||
end
|
||||
if not Kit.mouseDown then
|
||||
p.verticalRoll = nil
|
||||
end
|
||||
if p.slider or p.roll then
|
||||
Kit._dragDelta = 0
|
||||
end
|
||||
if
|
||||
p.wheelRect
|
||||
and not Kit.blockClicks
|
||||
and Kit.hit(p.wheelRect.x, p.wheelRect.y, p.wheelRect.w, p.wheelRect.h)
|
||||
and Kit.wheelY ~= 0
|
||||
then
|
||||
spinWheel()
|
||||
end
|
||||
p.scroll = Kit.scrollPixels(
|
||||
cx,
|
||||
bodyY,
|
||||
inner,
|
||||
bodyH,
|
||||
p.scroll,
|
||||
p.contentH or (6 * row + 7 * gap + 2 * Kit.textHeight("tiny"))
|
||||
)
|
||||
Kit.pushClip(cx, bodyY, inner, bodyH)
|
||||
cy = cy - p.scroll
|
||||
local contentStart = cy
|
||||
Kit.textCenter("title", tostring(p.value), cx, cy + gap, inner, PAL.heading)
|
||||
cy = cy + row + gap
|
||||
local ratio = (p.value - l.lo) / math.max(1, l.hi - l.lo)
|
||||
local trackX, trackW = cx + 12 * s, inner - 24 * s
|
||||
p.sliderRect = { x = cx, y = cy, w = inner, h = row }
|
||||
local trackY = cy + row / 2
|
||||
Theme.fillRounded(trackX, trackY - 4 * s, trackW, 8 * s, PAL.cardBorder, 0.4, 4 * s)
|
||||
if ratio > 0 then
|
||||
Theme.fillRounded(trackX, trackY - 4 * s, trackW * ratio, 8 * s, PAL.blue, 1, 4 * s)
|
||||
end
|
||||
Theme.col(PAL.blue)
|
||||
love.graphics.circle("fill", trackX + trackW * ratio, trackY, 11 * s)
|
||||
if Kit.press(cx, cy, inner, row) then
|
||||
setValue(math.floor(l.lo + Theme.clamp((Kit.mouseX - trackX) / trackW, 0, 1) * (l.hi - l.lo) + 0.5))
|
||||
end
|
||||
if not Kit.blockClicks and Kit.mouseDown and Kit.hit(cx, cy, inner, row) then
|
||||
p.slider = true
|
||||
end
|
||||
if p.slider and Kit.mouseDown then
|
||||
setValue(math.floor(l.lo + Theme.clamp((Kit.mouseX - trackX) / trackW, 0, 1) * (l.hi - l.lo) + 0.5))
|
||||
else
|
||||
p.slider = nil
|
||||
end
|
||||
cy = cy + row
|
||||
Kit.text("tiny", tostring(l.lo), cx, cy, PAL.caption)
|
||||
Kit.textRight("tiny", "Max " .. l.hi, cx + inner, cy, PAL.caption)
|
||||
cy = cy + Kit.textHeight("tiny") + gap
|
||||
-- A horizontal number wheel: one tick per value, with a stable centre.
|
||||
local tick = math.max(30 * s, Kit.textWidth("small", tostring(l.hi)) + 16 * s)
|
||||
p.wheelRect = { x = cx, y = cy, w = inner, h = row }
|
||||
Theme.fillRounded(cx, cy, inner, row, PAL.rowBg, 0.6, 12 * s)
|
||||
Theme.fillRounded(cx + (inner - tick) / 2, cy + 4 * s, tick, row - 8 * s, PAL.blue, 0.12, 8 * s)
|
||||
Kit.pushClip(cx, cy, inner, row)
|
||||
for i = -5, 5 do
|
||||
local n = p.value + i
|
||||
if n >= l.lo and n <= l.hi then
|
||||
Kit.textCenter(
|
||||
"small",
|
||||
tostring(n),
|
||||
cx + inner / 2 + i * tick - tick / 2,
|
||||
cy + (row - Kit.textHeight("small")) / 2,
|
||||
tick,
|
||||
i == 0 and PAL.blueInk or PAL.faint
|
||||
)
|
||||
end
|
||||
end
|
||||
Kit.popClip()
|
||||
if not Kit.blockClicks and Kit.mouseDown and Kit.hit(cx, cy, inner, row) and not p.roll and not p.verticalRoll then
|
||||
p.roll = { x = Kit.mouseX, y = Kit.mouseY, value = p.value }
|
||||
end
|
||||
if p.roll and Kit.mouseDown then
|
||||
setValue(p.roll.value + math.floor((p.roll.x - Kit.mouseX) / (tick / 2) + 0.5))
|
||||
else
|
||||
p.roll = nil
|
||||
end
|
||||
if Kit.hit(cx, cy, inner, row) and not Kit.blockClicks and Kit.wheelY ~= 0 then
|
||||
spinWheel()
|
||||
end
|
||||
cy = cy + row + gap
|
||||
Kit.textCenter("tiny", "Roll the numbers to fine-tune", cx, cy, inner, PAL.caption)
|
||||
cy = cy + Kit.textHeight("tiny") + gap
|
||||
local bw = (inner - 3 * gap) / 4
|
||||
for i, d in ipairs({ -5, -1, 1, 5 }) do
|
||||
local label = (d > 0 and "+" or "") .. d
|
||||
if
|
||||
Kit.button(
|
||||
cx + (i - 1) * (bw + gap),
|
||||
cy,
|
||||
bw,
|
||||
row,
|
||||
label,
|
||||
{ font = "small", enabled = p.value + d >= l.lo and p.value + d <= l.hi }
|
||||
)
|
||||
then
|
||||
setValue(p.value + d)
|
||||
end
|
||||
end
|
||||
cy = cy + row + gap
|
||||
local half = (inner - gap) / 2
|
||||
if Kit.button(cx, cy, half, row, "Min", { font = "small" }) then
|
||||
setValue(l.lo)
|
||||
end
|
||||
if Kit.button(cx + half + gap, cy, half, row, "Max", { font = "small", kind = "good" }) then
|
||||
setValue(l.hi)
|
||||
end
|
||||
cy = cy + row + gap
|
||||
if p.typing then
|
||||
p.draft = Kit.textfield("touch-exact", cx, cy, inner, row, p.draft, "Exact value")
|
||||
elseif Kit.button(cx, cy, inner, row, "Type a value", { font = "small", icon = "pencil" }) then
|
||||
p.typing, p.draft = true, tostring(p.value)
|
||||
end
|
||||
cy = cy + row + gap
|
||||
if p.error then
|
||||
cy = cy + Kit.textWrapped("tiny", p.error, cx, cy, inner, PAL.red) + gap
|
||||
end
|
||||
p.contentH = cy - contentStart
|
||||
Kit.popClip()
|
||||
if Kit.button(cx, y + h - pad - row, inner, row, "Apply", { kind = "good", font = "small", icon = "check" }) then
|
||||
E.commit(S, Kit)
|
||||
end
|
||||
end
|
||||
return E
|
||||
@@ -0,0 +1,88 @@
|
||||
local Gen = require("Gen")
|
||||
local MonOps = require("MonOps")
|
||||
local L = {}
|
||||
L.EV_KEYS = { "hp", "atk", "def", "spa", "spd", "spe" }
|
||||
function L.evTotal(mon)
|
||||
local total = 0
|
||||
for _, k in ipairs(L.EV_KEYS) do
|
||||
total = total + (tonumber(mon.evs and mon.evs[k]) or 0)
|
||||
end
|
||||
return total
|
||||
end
|
||||
function L.expAt(S, mon, level)
|
||||
local def = S.data and S.data.pokemon and S.data.pokemon[mon.species or mon.speciesId]
|
||||
local g = Gen.ofState(S)
|
||||
if g == 3 then
|
||||
return require("src.core.game3.summary_data").expForLevel(
|
||||
require("src.core.game3.pokemon").growthRate(mon.species or mon.speciesId),
|
||||
level
|
||||
)
|
||||
elseif g == 2 then
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
return Mon.experienceForLevel(Mon.growthFor(S.data, def and def.growthRate), level)
|
||||
end
|
||||
return require("src.pokemon.Growth").expForLevel(
|
||||
def and def.growthRate or 0,
|
||||
level,
|
||||
S.data.growth_rates
|
||||
)
|
||||
end
|
||||
function L.mon(S, mon, id)
|
||||
local egg = mon.egg or mon.isEgg
|
||||
local lo, hi, help = 0, 255, "Slide for big changes. Roll the numbers for small ones."
|
||||
local key = id:match("^ev%-(.+)")
|
||||
if key then
|
||||
hi =
|
||||
math.max(0, math.min(255, 510 - L.evTotal(mon) + (tonumber(mon.evs and mon.evs[key]) or 0)))
|
||||
help = "Training points. All six stats share 510. Lower one to free up another."
|
||||
elseif id:match("^iv%-") then
|
||||
hi, help = 31, "Natural stat strength. Higher is stronger."
|
||||
elseif id:match("^dv%-") then
|
||||
hi, help = 15, "Natural stat strength. These also set HP, and in Gen 2, gender and shininess."
|
||||
elseif id:match("^se%-") then
|
||||
hi, help = 65535, "Stat training. Each stat has its own limit."
|
||||
elseif id == "level" then
|
||||
lo, hi, help =
|
||||
egg and 5 or math.min(100, math.max(1, tonumber(mon.metLevel) or 1)),
|
||||
egg and 5 or 100,
|
||||
"Changes level and recalculates stats. Moves stay the same."
|
||||
elseif id == "experience" then
|
||||
lo, hi, help =
|
||||
L.expAt(S, mon, egg and 5 or math.min(100, math.max(1, tonumber(mon.metLevel) or 1))),
|
||||
L.expAt(S, mon, egg and 5 or 100),
|
||||
"Total experience. Level updates to match."
|
||||
elseif id == "hp" or id == "current-hp" then
|
||||
hi, help =
|
||||
mon.maxHp or (mon.stats and mon.stats.hp) or 0,
|
||||
"Current health. The limit follows level and stats. Max fills it."
|
||||
elseif id == "happiness" or id == "friendship" then
|
||||
help = egg and "Egg hatch countdown. Lower means closer to hatching."
|
||||
or "Friendship. Max makes it as friendly as it can be."
|
||||
elseif id:match("^ppup%-%d") or id:match("^pp%-%d") then
|
||||
local slot = tonumber(id:match("(%d+)$"))
|
||||
local base = MonOps.getBasePp(S.data, mon, slot)
|
||||
if id:match("^ppup%-") then
|
||||
hi, help =
|
||||
(egg or base == 1) and 0 or 3, "Raises this move's PP limit. Then use Max on PP to fill it."
|
||||
else
|
||||
hi = MonOps.calcMaxPp(base, MonOps.getPpUps(mon, slot), Gen.ofState(S))
|
||||
help = "Uses left for this move. PP Ups raise the limit."
|
||||
end
|
||||
else
|
||||
local d = require("Properties").find(S, id)
|
||||
if d then
|
||||
lo, hi, help = d.lo or 0, d.hi or 255, d.help or help
|
||||
end
|
||||
if id == "metLevel" then
|
||||
hi = math.min(hi, tonumber(mon.level) or hi)
|
||||
end
|
||||
end
|
||||
return {
|
||||
lo = lo,
|
||||
hi = hi,
|
||||
help = help,
|
||||
step = id == "level" and 5 or 1,
|
||||
remaining = key and math.max(0, 510 - L.evTotal(mon)) or nil,
|
||||
}
|
||||
end
|
||||
return L
|
||||
@@ -143,10 +143,9 @@ local function drawView(S, Kit, x, y, w, h)
|
||||
end
|
||||
local bodyH = math.max(0, footer - gap - cy)
|
||||
if S.boxView == "party" then
|
||||
local visible = math.max(1, math.floor(bodyH / (row + gap)))
|
||||
S.dockOffset = Kit.scroll(cx, cy, inner, bodyH, S.dockOffset or 0, #S.save.party, visible)
|
||||
local drawn, shift = Kit.list(S, "dockOffset", cx, cy, inner, bodyH, #S.save.party, row + gap)
|
||||
Kit.pushClip(cx, cy, inner, bodyH)
|
||||
for i = 1, visible do
|
||||
for i = 1, drawn do
|
||||
local slot = S.dockOffset + i
|
||||
local mon = S.save.party[slot]
|
||||
if not mon then
|
||||
@@ -155,7 +154,7 @@ local function drawView(S, Kit, x, y, w, h)
|
||||
if
|
||||
Kit.button(
|
||||
cx,
|
||||
cy + (i - 1) * (row + gap),
|
||||
cy + (i - 1) * (row + gap) - shift,
|
||||
inner,
|
||||
row,
|
||||
tostring(slot)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
local L = require("Legality")
|
||||
local PAL = require("Theme").PAL
|
||||
local M = {}
|
||||
local Touch = require("TouchEditor")
|
||||
local Motion = require("Motion")
|
||||
function M.draw(S, Kit, x, y, w, h)
|
||||
Kit.card(x, y, w, h)
|
||||
@@ -11,7 +12,7 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
S._checksRevision, S._checksSave = S.revision, S.save
|
||||
end
|
||||
local r = S.checkReport
|
||||
local message = ("%d Pokemon / %d property errors. Encounter legality remains unchecked."):format(
|
||||
local message = ("%d Pokémon · %d errors. Origin checks still need review."):format(
|
||||
#r.entries,
|
||||
r.errors
|
||||
)
|
||||
@@ -23,6 +24,20 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
inner,
|
||||
r.errors > 0 and PAL.red or PAL.yellow
|
||||
) + gap
|
||||
local actionH = Touch.action(
|
||||
S,
|
||||
Kit,
|
||||
"Fix all errors",
|
||||
"Fixes invalid values, stats and PP across your party and boxes. Anything needing a manual choice stays for review.",
|
||||
function()
|
||||
require("Ops").fixAllErrors(S)
|
||||
end,
|
||||
cx,
|
||||
y + pad + head,
|
||||
inner,
|
||||
"good"
|
||||
)
|
||||
head = head + actionH + gap
|
||||
local top = y + pad + head
|
||||
local bodyH = math.max(0, h - 2 * pad - head)
|
||||
S.checkScroll =
|
||||
|
||||
@@ -99,15 +99,14 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
local gridH = math.max(0, pagerY - 12 * s - gridTop)
|
||||
local perCol = math.max(1, math.floor((gridH + gap) / (cardH + gap)))
|
||||
local perPage = perCol * cols
|
||||
S.dexOffset = Ops.clamp(S.dexOffset or 0, 0, math.max(0, #species - perPage))
|
||||
S.dexOffset = Kit.scroll(cx, gridTop, inner, gridH, S.dexOffset, #species, perPage, cols)
|
||||
local drawn, shift = Kit.list(S, "dexOffset", cx, gridTop, inner, gridH, #species, cardH + gap, cols)
|
||||
local chipW = (colW - 2 * cardPad - gap) / 2
|
||||
local ownedKey = Gen.dexOwnedKey(S.save)
|
||||
Kit.pushClip(cx, gridTop, inner, gridH)
|
||||
for i = 1, math.min(perPage, #species - S.dexOffset) do
|
||||
for i = 1, drawn do
|
||||
local id = species[S.dexOffset + i]
|
||||
local ci, ri = (i - 1) % cols, math.floor((i - 1) / cols)
|
||||
local rx, ry = cx + ci * (colW + gap), gridTop + ri * (cardH + gap)
|
||||
local rx, ry = cx + ci * (colW + gap), gridTop + ri * (cardH + gap) - shift
|
||||
local def = S.data.pokemon[id]
|
||||
local spId = def and (def.speciesId or def.dex)
|
||||
local isSeen = dex.seen[id] == true or (spId and dex.seen[spId] == true)
|
||||
@@ -145,7 +144,7 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
end
|
||||
end
|
||||
Kit.popClip()
|
||||
Kit.scrollbar(cx, gridTop, inner, gridH, S.dexOffset, #species, perPage)
|
||||
Kit.listScrollbar(S, "dexOffset", cx, gridTop, inner, gridH)
|
||||
S.dexOffset = Kit.pager(cx, pagerY, inner, S.dexOffset, #species, perPage)
|
||||
end
|
||||
return M
|
||||
|
||||
@@ -354,8 +354,7 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
local perCol = math.max(1, math.floor(gridH / (rowH + rowGap)))
|
||||
local perPage = perCol * cols
|
||||
|
||||
S.eventsOffset = Ops.clamp(S.eventsOffset or 0, 0, math.max(0, #rows - perPage))
|
||||
S.eventsOffset = Kit.scroll(cx, gridTop, inner, gridH, S.eventsOffset, #rows, perPage, cols)
|
||||
local drawn, shift = Kit.list(S, "eventsOffset", cx, gridTop, inner, gridH, #rows, rowH + rowGap, cols)
|
||||
|
||||
if #rows == 0 then
|
||||
Kit.emptyBox(
|
||||
@@ -368,12 +367,12 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
end
|
||||
|
||||
Kit.pushClip(cx, gridTop, inner, gridH)
|
||||
for i = 1, math.min(perPage, #rows - S.eventsOffset) do
|
||||
for i = 1, drawn do
|
||||
local row = rows[S.eventsOffset + i]
|
||||
local ci = (i - 1) % cols
|
||||
local ri = math.floor((i - 1) / cols)
|
||||
local rx = cx + ci * (colW + colGap)
|
||||
local ry = gridTop + ri * (rowH + rowGap)
|
||||
local ry = gridTop + ri * (rowH + rowGap) - shift
|
||||
if row.header then
|
||||
Kit.text(
|
||||
"mono",
|
||||
@@ -438,7 +437,7 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
end
|
||||
Kit.popClip()
|
||||
|
||||
Kit.scrollbar(cx, gridTop, inner, gridH, S.eventsOffset, #rows, perPage)
|
||||
Kit.listScrollbar(S, "eventsOffset", cx, gridTop, inner, gridH)
|
||||
|
||||
-- "Clear all" button
|
||||
if clearKey then
|
||||
|
||||
@@ -141,21 +141,20 @@ function Picker.draw(S, Kit, width, height)
|
||||
local hits = Picker.results(S)
|
||||
local listH, rowH, rowGap, pagerH = PickerChrome.listMetrics(Kit, y, h, pad, cy)
|
||||
local perPage = math.max(1, math.floor((listH + rowGap) / (rowH + rowGap)))
|
||||
p.offset = Theme.clamp(p.offset or 0, 0, math.max(0, #hits - perPage))
|
||||
-- wheel / touch drag scroll the modal list too; the shield is already
|
||||
-- lowered for this layer, so Kit.scroll works here and only here
|
||||
p.offset = Kit.scroll(cx, cy, inner, listH, p.offset, #hits, perPage)
|
||||
local drawn, shift = Kit.list(p, "offset", cx, cy, inner, listH, #hits, rowH + rowGap)
|
||||
|
||||
if #hits == 0 then
|
||||
Kit.emptyBox(cx, cy, inner, listH, "Nothing matches that.")
|
||||
else
|
||||
Kit.pushClip(cx, cy, inner, listH)
|
||||
for i = 1, perPage do
|
||||
for i = 1, drawn do
|
||||
local id = hits[p.offset + i]
|
||||
if not id then
|
||||
break
|
||||
end
|
||||
local ry = cy + (i - 1) * (rowH + rowGap)
|
||||
local ry = cy + (i - 1) * (rowH + rowGap) - shift
|
||||
if Kit.row(cx, ry, inner, rowH, false, PAL.green, 9 * s) then
|
||||
Picker.commit(S, Kit, id)
|
||||
end
|
||||
@@ -185,7 +184,7 @@ function Picker.draw(S, Kit, width, height)
|
||||
end
|
||||
end
|
||||
Kit.popClip()
|
||||
Kit.scrollbar(cx, cy, inner, listH, p.offset, #hits, perPage)
|
||||
Kit.listScrollbar(p, "offset", cx, cy, inner, listH)
|
||||
end
|
||||
|
||||
p.offset = Kit.pager(cx, y + h - pad - pagerH, inner, p.offset, #hits, perPage)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
local Ops = require("Ops")
|
||||
local Gen = require("Gen")
|
||||
local Bag = require("src.inventory.Bag")
|
||||
local PAL = require("Theme").PAL
|
||||
local M = {}
|
||||
local Touch = require("TouchEditor")
|
||||
local Motion = require("Motion")
|
||||
local Chooser = require("Chooser")
|
||||
local function drawView(S, Kit, x, y, w, h)
|
||||
@@ -62,44 +62,34 @@ local function drawView(S, Kit, x, y, w, h)
|
||||
cy = cy + row + gap
|
||||
end
|
||||
if S.itemView == "wallet" then
|
||||
local ch = 2 * (row + gap + Kit.textHeight("small") + gap) + (row + gap) * 2
|
||||
local bodyH = math.max(0, y + h - pad - cy)
|
||||
S.walletScroll =
|
||||
Kit.scrollPixels(cx, cy, inner, math.max(0, y + h - pad - cy), S.walletScroll or 0, ch)
|
||||
Kit.pushClip(cx, cy, inner, math.max(0, y + h - pad - cy))
|
||||
cy = cy - S.walletScroll
|
||||
Kit.scrollPixels(cx, cy, inner, bodyH, S.walletScroll or 0, S._walletHeight or 0)
|
||||
Kit.pushClip(cx, cy, inner, bodyH)
|
||||
local start = cy - S.walletScroll
|
||||
cy = start
|
||||
for _, f in ipairs({
|
||||
{ "money", "Money", Gen.money(S.save), Ops.maxMoney },
|
||||
{ "coins", "Coins", Gen.coins(S.save), Ops.maxCoins },
|
||||
{ "money", "Money", Gen.money(S.save), 999999 },
|
||||
{ "coins", "Coins", Gen.coins(S.save), 9999 },
|
||||
}) do
|
||||
Kit.text("small", f[2], cx, cy, PAL.text)
|
||||
cy = cy + Kit.textHeight("small") + gap
|
||||
S.walletDrafts = S.walletDrafts or {}
|
||||
local function apply(v)
|
||||
if Ops.setTrainerProperty(S, f[1], v) then
|
||||
S.walletDrafts[f[1]] = nil
|
||||
end
|
||||
end
|
||||
local bw = math.max(row, 64 * s)
|
||||
S.walletDrafts[f[1]] = Kit.textfield(
|
||||
"wallet-" .. f[1],
|
||||
cx,
|
||||
cy,
|
||||
inner - bw - gap,
|
||||
row,
|
||||
S.walletDrafts[f[1]] or tostring(f[3]),
|
||||
"value",
|
||||
{ onSubmit = apply }
|
||||
)
|
||||
if Kit.button(cx + inner - bw, cy, bw, row, "Set", { kind = "accent", font = "small" }) then
|
||||
apply(S.walletDrafts[f[1]])
|
||||
Kit.blur()
|
||||
end
|
||||
cy = cy + row + gap
|
||||
if Kit.button(cx, cy, inner, row, "Max " .. f[2], { kind = "good", font = "small" }) then
|
||||
f[4](S)
|
||||
end
|
||||
cy = cy + row + gap
|
||||
cy = cy
|
||||
+ Touch.value(
|
||||
S,
|
||||
Kit,
|
||||
"wallet-" .. f[1],
|
||||
f[2],
|
||||
f[3],
|
||||
{ lo = 0, hi = f[4], help = "Max fills it. Type a value for an exact amount." },
|
||||
cx,
|
||||
cy,
|
||||
inner,
|
||||
function(v)
|
||||
return Ops.setTrainerProperty(S, f[1], v)
|
||||
end
|
||||
)
|
||||
+ gap
|
||||
end
|
||||
S._walletHeight = cy - start
|
||||
Kit.popClip()
|
||||
return
|
||||
elseif S.itemView == "badges" then
|
||||
@@ -189,30 +179,53 @@ local function drawView(S, Kit, x, y, w, h)
|
||||
local actionMin = Kit.buttonWidth("Confirm?", { font = "small", iconStack = true }, row)
|
||||
local actionCols = inner >= 5 * actionMin + 4 * gap and 5 or 3
|
||||
local actionRows = math.ceil(5 / actionCols)
|
||||
local itemH = actionRows * row + (actionRows - 1) * gap + Kit.textHeight("small") + 3 * gap
|
||||
local itemH = actionRows * row + (actionRows - 1) * gap + row + 3 * gap
|
||||
local visible = math.max(1, math.floor(bodyH / itemH))
|
||||
local offsetKey = prefix .. "Offset"
|
||||
S[offsetKey] = Kit.scroll(cx, cy, inner, bodyH, S[offsetKey] or 0, #order, visible)
|
||||
local drawn, shift = Kit.list(S, offsetKey, cx, cy, inner, bodyH, #order, itemH)
|
||||
Kit.pushClip(cx, cy, inner, bodyH)
|
||||
for i = 1, visible do
|
||||
for i = 1, drawn do
|
||||
local id = order[S[offsetKey] + i]
|
||||
if id == nil then
|
||||
break
|
||||
end
|
||||
local by = cy + (i - 1) * itemH
|
||||
local by = cy + (i - 1) * itemH - shift
|
||||
local def = S.data.items[id]
|
||||
Kit.text(
|
||||
"small",
|
||||
Kit.ellipsize(
|
||||
"small",
|
||||
tostring(def and def.name or id) .. " x" .. tostring(quantities[id] or 0),
|
||||
inner
|
||||
),
|
||||
cx,
|
||||
by,
|
||||
PAL.text
|
||||
)
|
||||
by = by + Kit.textHeight("small") + gap
|
||||
local count = quantities[id] or 0
|
||||
local max = Ops.itemMax(S, id, pc)
|
||||
local issue = not require("Legality").integer(count, 1, max)
|
||||
and ("Saved stack must be a whole number from 1 to " .. max) or nil
|
||||
local opts = { font = "small", align = "left", trailingIcon = "pencil", face = "invert", invalid = issue ~= nil }
|
||||
if
|
||||
Kit.button(
|
||||
cx,
|
||||
by,
|
||||
inner,
|
||||
row,
|
||||
tostring(def and def.name or id):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
.. " ×"
|
||||
.. count,
|
||||
opts
|
||||
)
|
||||
then
|
||||
Touch.open(S, Kit, {
|
||||
mode = "number",
|
||||
id = "item-" .. tostring(id),
|
||||
title = def and def.name or tostring(id),
|
||||
value = count,
|
||||
savedValue = count,
|
||||
issue = issue,
|
||||
limits = {
|
||||
lo = 1,
|
||||
hi = max,
|
||||
help = "Set the stack size. Max fills it; Drop removes it.",
|
||||
},
|
||||
apply = function(v)
|
||||
return call("Adjust", id, v - count)
|
||||
end,
|
||||
})
|
||||
end
|
||||
by = by + row + gap
|
||||
local labels = {
|
||||
"Decrease",
|
||||
"Increase",
|
||||
|
||||
@@ -310,18 +310,17 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
local mGap = 4 * s
|
||||
local listBodyH = (compactList and lr.y + lr.h - pad or pagerY - 10 * s) - listTop
|
||||
local perPage = math.max(1, math.floor(listBodyH / (mRowH + mGap)))
|
||||
S.mapListOffset = Ops.clamp(S.mapListOffset or 0, 0, math.max(0, #ids - perPage))
|
||||
-- wheel and touch drag reach the list too (#715): App routes the wheel to
|
||||
-- zoom on this tab, so the list rides Kit's drag path and the pager alone
|
||||
-- on desktop -- on a phone the drag is the difference between "stuck" and
|
||||
-- scrollable.
|
||||
S.mapListOffset =
|
||||
Kit.scroll(lr.x + pad, listTop, listInner, listBodyH, S.mapListOffset, #ids, perPage)
|
||||
local drawn, shift =
|
||||
Kit.list(S, "mapListOffset", lr.x + pad, listTop, listInner, listBodyH, #ids, mRowH + mGap)
|
||||
|
||||
Kit.pushClip(lr.x + pad, listTop, listInner, math.max(0, listBodyH))
|
||||
for i = 1, math.min(perPage, #ids - S.mapListOffset) do
|
||||
for i = 1, drawn do
|
||||
local id = ids[S.mapListOffset + i]
|
||||
local ry = listTop + (i - 1) * (mRowH + mGap)
|
||||
local ry = listTop + (i - 1) * (mRowH + mGap) - shift
|
||||
if Kit.row(lr.x + pad, ry, listInner, mRowH, id == S.mapId, PAL.blue, 7 * s) then
|
||||
MapBrowser.select(S, id)
|
||||
if stacked then
|
||||
@@ -340,7 +339,7 @@ local function drawSection(S, Kit, x, y, w, h)
|
||||
Kit.text("mono", "no map matches", lr.x + pad + 9 * s, listTop + 8 * s, PAL.faint)
|
||||
end
|
||||
Kit.popClip()
|
||||
Kit.scrollbar(lr.x + pad, listTop, listInner, listBodyH, S.mapListOffset, #ids, perPage)
|
||||
Kit.listScrollbar(S, "mapListOffset", lr.x + pad, listTop, listInner, listBodyH)
|
||||
if not compactList then
|
||||
S.mapListOffset = Kit.pager(lr.x + pad, pagerY, listInner, S.mapListOffset, #ids, perPage)
|
||||
end
|
||||
|
||||
@@ -103,10 +103,9 @@ function Picker.draw(S, Kit, width, height)
|
||||
local hits = Picker.results(S)
|
||||
local listH, rowH, rowGap, pagerH = PickerChrome.listMetrics(Kit, y, h, pad, cy)
|
||||
local perPage = math.max(1, math.floor((listH + rowGap) / (rowH + rowGap)))
|
||||
p.offset = Theme.clamp(p.offset or 0, 0, math.max(0, #hits - perPage))
|
||||
-- wheel / touch drag scroll the modal list too; the shield is already
|
||||
-- lowered for this layer, so Kit.scroll works here and only here (#715)
|
||||
p.offset = Kit.scroll(cx, cy, inner, listH, p.offset, #hits, perPage)
|
||||
local drawn, shift = Kit.list(p, "offset", cx, cy, inner, listH, #hits, rowH + rowGap)
|
||||
|
||||
local mon = S.editingMon
|
||||
local mv = mon and mon.moves and mon.moves[p.slot]
|
||||
@@ -116,12 +115,12 @@ function Picker.draw(S, Kit, width, height)
|
||||
Kit.emptyBox(cx, cy, inner, listH, "Nothing matches that.")
|
||||
else
|
||||
Kit.pushClip(cx, cy, inner, listH)
|
||||
for i = 1, perPage do
|
||||
for i = 1, drawn do
|
||||
local id = hits[p.offset + i]
|
||||
if not id then
|
||||
break
|
||||
end
|
||||
local ry = cy + (i - 1) * (rowH + rowGap)
|
||||
local ry = cy + (i - 1) * (rowH + rowGap) - shift
|
||||
local def = S.data.moves[id]
|
||||
local usable = Ops.moveUsable(S, id)
|
||||
local current = (currentId ~= nil)
|
||||
@@ -162,7 +161,7 @@ function Picker.draw(S, Kit, width, height)
|
||||
)
|
||||
end
|
||||
Kit.popClip()
|
||||
Kit.scrollbar(cx, cy, inner, listH, p.offset, #hits, perPage)
|
||||
Kit.listScrollbar(p, "offset", cx, cy, inner, listH)
|
||||
end
|
||||
|
||||
p.offset = Kit.pager(cx, y + h - pad - pagerH, inner, p.offset, #hits, perPage)
|
||||
|
||||
@@ -67,17 +67,15 @@ local function drawRoster(S, Kit, x, y, listW, h)
|
||||
-- The list used to `break` past the fold, silently hiding party slots on
|
||||
-- a short window; it scrolls instead now (#715), same offset contract as
|
||||
-- every other list in the editor.
|
||||
local visible = math.max(1, math.floor((listH + rowGap) / (rowH + rowGap)))
|
||||
S.partyOffset =
|
||||
Kit.scroll(cx, listTop, innerW, listH, S.partyOffset or 0, #S.save.party, visible)
|
||||
local drawn, shift = Kit.list(S, "partyOffset", cx, listTop, innerW, listH, #S.save.party, rowH + rowGap)
|
||||
Kit.pushClip(cx, listTop, innerW, listH)
|
||||
for i = 1, visible do
|
||||
for i = 1, drawn do
|
||||
local slot = S.partyOffset + i
|
||||
local mon = S.save.party[slot]
|
||||
if not mon then
|
||||
break
|
||||
end
|
||||
local ry = listTop + (i - 1) * (rowH + rowGap)
|
||||
local ry = listTop + (i - 1) * (rowH + rowGap) - shift
|
||||
local selected = (S.editingMon == mon)
|
||||
if Kit.row(cx, ry, innerW, rowH, selected, PAL.green) then
|
||||
Ops.selectParty(S, slot)
|
||||
@@ -146,7 +144,7 @@ local function drawRoster(S, Kit, x, y, listW, h)
|
||||
)
|
||||
end
|
||||
Kit.popClip()
|
||||
Kit.scrollbar(cx, listTop, innerW, listH, S.partyOffset, #S.save.party, visible)
|
||||
Kit.listScrollbar(S, "partyOffset", cx, listTop, innerW, listH)
|
||||
end
|
||||
|
||||
local halfW = (innerW - 10 * s) / 2
|
||||
|
||||
@@ -114,21 +114,20 @@ function Picker.draw(S, Kit, width, height)
|
||||
local hits = Picker.results(S)
|
||||
local listH, rowH, rowGap, pagerH = PickerChrome.listMetrics(Kit, y, h, pad, cy)
|
||||
local perPage = math.max(1, math.floor((listH + rowGap) / (rowH + rowGap)))
|
||||
p.offset = Theme.clamp(p.offset or 0, 0, math.max(0, #hits - perPage))
|
||||
-- wheel / touch drag scroll the modal list too; the shield is already
|
||||
-- lowered for this layer, so Kit.scroll works here and only here (#715)
|
||||
p.offset = Kit.scroll(cx, cy, inner, listH, p.offset, #hits, perPage)
|
||||
local drawn, shift = Kit.list(p, "offset", cx, cy, inner, listH, #hits, rowH + rowGap)
|
||||
|
||||
if #hits == 0 then
|
||||
Kit.emptyBox(cx, cy, inner, listH, "Nothing matches that.")
|
||||
else
|
||||
Kit.pushClip(cx, cy, inner, listH)
|
||||
for i = 1, perPage do
|
||||
for i = 1, drawn do
|
||||
local id = hits[p.offset + i]
|
||||
if not id then
|
||||
break
|
||||
end
|
||||
local ry = cy + (i - 1) * (rowH + rowGap)
|
||||
local ry = cy + (i - 1) * (rowH + rowGap) - shift
|
||||
local def = S.data.pokemon[id]
|
||||
-- A record the formulas cannot use still lists, greyed: hiding it would
|
||||
-- make a modded species look like it never registered (#541).
|
||||
@@ -165,7 +164,7 @@ function Picker.draw(S, Kit, width, height)
|
||||
)
|
||||
end
|
||||
Kit.popClip()
|
||||
Kit.scrollbar(cx, cy, inner, listH, p.offset, #hits, perPage)
|
||||
Kit.listScrollbar(p, "offset", cx, cy, inner, listH)
|
||||
end
|
||||
|
||||
p.offset = Kit.pager(cx, y + h - pad - pagerH, inner, p.offset, #hits, perPage)
|
||||
|
||||
@@ -2,6 +2,7 @@ local Ops = require("Ops")
|
||||
local Gen = require("Gen")
|
||||
local PAL = require("Theme").PAL
|
||||
local M = {}
|
||||
local Touch = require("TouchEditor")
|
||||
function M.draw(S, Kit, x, y, w, h)
|
||||
Kit.card(x, y, w, h)
|
||||
local pad, gap, row = 14 * Kit.scale, 10 * Kit.scale, Kit.controlH()
|
||||
@@ -13,45 +14,71 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
"Trainer name",
|
||||
(S.save.player and S.save.player.name) or S.save.name or S.save.playerName or "",
|
||||
},
|
||||
{ "id", "Trainer ID (0-65535)", (S.save.player and S.save.player.id) or S.save.trainerId or 0 },
|
||||
{ "money", "Money (0-999999)", Gen.money(S.save) },
|
||||
{ "coins", "Coins (0-9999)", Gen.coins(S.save) },
|
||||
{ "id", "Trainer ID", (S.save.player and S.save.player.id) or S.save.trainerId or 0 },
|
||||
{ "money", "Money", Gen.money(S.save) },
|
||||
{ "coins", "Coins", Gen.coins(S.save) },
|
||||
}
|
||||
if g == 3 then
|
||||
table.insert(fields, 3, { "secretId", "Secret ID (0-65535)", S.save.secretId or 0 })
|
||||
table.insert(fields, 3, { "secretId", "Secret ID", S.save.secretId or 0 })
|
||||
end
|
||||
local contentH = pad * 2
|
||||
+ #fields * (Kit.textHeight("small") + gap + row + gap)
|
||||
+ (Gen.hasPlayerGender(S.save, S.version) and row + gap or 0)
|
||||
local contentH = S._trainerHeight or 0
|
||||
S.trainerScroll = Kit.scrollPixels(x, y, w, h, S.trainerScroll or 0, contentH)
|
||||
Kit.pushClip(x, y, w, h)
|
||||
local cy = y + pad - S.trainerScroll
|
||||
S.trainerDrafts = S.trainerDrafts or {}
|
||||
for _, f in ipairs(fields) do
|
||||
Kit.text("small", f[2], cx, cy, PAL.text)
|
||||
cy = cy + Kit.textHeight("small") + gap
|
||||
local key = "trainer-" .. f[1]
|
||||
local setW = math.max(row, 64 * Kit.scale)
|
||||
local function apply(v)
|
||||
if Ops.setTrainerProperty(S, f[1], v) then
|
||||
S.trainerDrafts[f[1]] = nil
|
||||
if f[1] ~= "name" then
|
||||
local hi = ({ id = 65535, secretId = 65535, money = 999999, coins = 9999 })[f[1]]
|
||||
cy = cy
|
||||
+ Touch.value(
|
||||
S,
|
||||
Kit,
|
||||
"trainer-" .. f[1],
|
||||
f[2],
|
||||
f[3],
|
||||
{
|
||||
lo = 0,
|
||||
hi = hi,
|
||||
help = f[1] == "money" and "Your wallet. Max fills it."
|
||||
or f[1] == "coins" and "Game Corner coins. Max fills the coin case."
|
||||
or "Part of your trainer identity. Changing it can affect who owns a Pokémon.",
|
||||
},
|
||||
cx,
|
||||
cy,
|
||||
inner,
|
||||
function(v)
|
||||
return Ops.setTrainerProperty(S, f[1], v)
|
||||
end
|
||||
)
|
||||
+ gap
|
||||
else
|
||||
Kit.text("small", f[2], cx, cy, PAL.text)
|
||||
cy = cy + Kit.textHeight("small") + gap
|
||||
local key = "trainer-" .. f[1]
|
||||
local setW = math.max(row, 64 * Kit.scale)
|
||||
local function apply(v)
|
||||
if Ops.setTrainerProperty(S, f[1], v) then
|
||||
S.trainerDrafts[f[1]] = nil
|
||||
end
|
||||
end
|
||||
S.trainerDrafts[f[1]] = Kit.textfield(
|
||||
key,
|
||||
cx,
|
||||
cy,
|
||||
inner - setW - gap,
|
||||
row,
|
||||
S.trainerDrafts[f[1]] or tostring(f[3]),
|
||||
"Name",
|
||||
{ onSubmit = apply }
|
||||
)
|
||||
if
|
||||
Kit.button(cx + inner - setW, cy, setW, row, "Set", { kind = "accent", font = "small" })
|
||||
then
|
||||
apply(S.trainerDrafts[f[1]])
|
||||
Kit.blur()
|
||||
end
|
||||
cy = cy + row + gap
|
||||
end
|
||||
S.trainerDrafts[f[1]] = Kit.textfield(
|
||||
key,
|
||||
cx,
|
||||
cy,
|
||||
inner - setW - gap,
|
||||
row,
|
||||
S.trainerDrafts[f[1]] or tostring(f[3]),
|
||||
"value",
|
||||
{ onSubmit = apply }
|
||||
)
|
||||
if Kit.button(cx + inner - setW, cy, setW, row, "Set", { kind = "accent", font = "small" }) then
|
||||
apply(S.trainerDrafts[f[1]])
|
||||
Kit.blur()
|
||||
end
|
||||
cy = cy + row + gap
|
||||
end
|
||||
if Gen.hasPlayerGender(S.save, S.version) then
|
||||
local half = (inner - gap) / 2
|
||||
@@ -70,6 +97,11 @@ function M.draw(S, Kit, x, y, w, h)
|
||||
end
|
||||
end
|
||||
end
|
||||
S._trainerHeight = cy
|
||||
- (y + pad - S.trainerScroll)
|
||||
+ 2 * pad
|
||||
+ (Gen.hasPlayerGender(S.save, S.version) and row + gap or 0)
|
||||
Kit.popClip()
|
||||
Kit.scrollbar(x, y, w, h, S.trainerScroll, S._trainerHeight, h)
|
||||
end
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user