diff --git a/lib/OverworldBattle.lua b/lib/OverworldBattle.lua index 0d61c1c..447d45a 100644 --- a/lib/OverworldBattle.lua +++ b/lib/OverworldBattle.lua @@ -1108,10 +1108,24 @@ function OverworldBattle.sideTexture(battle, side) -- alone, which the engine's both-sides-at-once pic layer cannot do. local shinyTint = nil do - local battler = (side == "player") and battle.player or battle.enemy - local g2 = game() - shinyTint = battler and V.require("ShinyUI") - .tintFor(battler.mon, g2 and g2.data) or nil + -- NOT when this side is showing a PERSON. Both sides can be holding a + -- trainer pic rather than a Pokemon -- the foe's portrait before the + -- send-out, and the player's own back until "Go!" -- and a shiny is a + -- fact about a Pokemon, not about its owner. Tinting through it turned + -- the player's trainer sprite a different colour for the whole intro, + -- which is what a shiny Pokemon in the party looks like if you do not + -- ask this question. The two tests are the same ones sideTexture already + -- uses to label the finished texture, asked here instead of after. + local person = (side == "enemy" + and battle.showEnemyTrainer and battle.trainerPic) + or (side == "player" + and battle.showPlayerBack and battle.playerBackPic) + if not person then + local battler = (side == "player") and battle.player or battle.enemy + local g2 = game() + shinyTint = battler and V.require("ShinyUI") + .tintFor(battler.mon, g2 and g2.data) or nil + end end local ok, err = pcall(function() diff --git a/lib/ShinyFx.lua b/lib/ShinyFx.lua index 33c1cd3..62c5dc2 100644 --- a/lib/ShinyFx.lua +++ b/lib/ShinyFx.lua @@ -163,12 +163,42 @@ end -- Start (or restart) the burst on one side. Restarting rather than ignoring -- a second call is deliberate: a shiny that faints and is sent back out -- should sparkle again. +-- ARMED, BUT NOT YET RUNNING. The clock does not start here, and that is the +-- whole point: the edge this is armed on -- a side's occupant changing -- +-- happens while the screen is still mid-WIPE, a second or more before the +-- battle draws a single frame. A burst that started its three-quarter-second +-- life at that moment was always over before anybody could see it, which is +-- exactly what "the sparkle isn't appearing" looked like: armed, drawn, +-- counted, and finished behind the transition. +-- +-- So `pending` holds it at frame zero until the scene actually draws this +-- side (see draw), and the life begins from there. function ShinyFx.arm(side) if side ~= "player" and side ~= "enemy" then return end - live[side] = { t = 0 } + live[side] = { t = 0, pending = true } + if ShinyFx.debug then ShinyFx.debug.armed = (ShinyFx.debug.armed or 0) + 1 end +end + +-- The fight is on screen now: let any burst waiting on this side begin. +-- +-- Split from arm because the two moments are genuinely different and were +-- conflated twice. Arming happens when the OCCUPANT changes, which is during +-- the transition; the burst may only start once the transition is OVER and +-- there is somebody watching. Between them it sits at zero. +function ShinyFx.release(side) + local s = live[side] + if s and s.pending then + s.pending = nil + if ShinyFx.debug then + ShinyFx.debug.released = (ShinyFx.debug.released or 0) + 1 + end + end end function ShinyFx.clear(side) + if ShinyFx.debug and side and live[side] then + ShinyFx.debug.cleared = (ShinyFx.debug.cleared or 0) + 1 + end if side then live[side] = nil else live.player, live.enemy = nil, nil end end @@ -181,7 +211,9 @@ function ShinyFx.update(dt) dt = dt or 0 for _, side in ipairs({ "player", "enemy" }) do local s = live[side] - if s then + -- a pending burst does not age: it is waiting for the scene to draw it + -- for the first time, which is when its life actually begins (see arm) + if s and not s.pending then s.t = s.t + dt if s.t >= ShinyFx.LIFE then live[side] = nil end end @@ -201,7 +233,7 @@ local function easeOut(u) return 1 - (1 - u) * (1 - u) end -- invisible to the test suite and this one has four separate ways to be a -- no-op, all of them silent. ShinyFx.debug = { calls = 0, noArena = 0, noImage = 0, noMesh = 0, - noLive = 0, quads = 0 } + noLive = 0, quads = 0, armed = 0, cleared = 0 } function ShinyFx.draw(arena, groundY, pull) local dbg = ShinyFx.debug @@ -220,6 +252,12 @@ function ShinyFx.draw(arena, groundY, pull) for _, side in ipairs({ "player", "enemy" }) do local s = live[side] local cell = (side == "player") and arena.player or arena.enemy + -- A pending burst is not drawn at all. It is waiting for the fight to be + -- ON SCREEN, which is not the same as the scene being drawn: the battle + -- renders underneath the transition wipe for a second or so first, and a + -- burst started there spends its whole life behind it. Stadium.release + -- is what says the wipe is done. + if s and s.pending then s = nil end if s and cell then local u = math.min(1, s.t / ShinyFx.LIFE) local e = easeOut(u) diff --git a/lib/Stadium.lua b/lib/Stadium.lua index e3a35ed..26bbcab 100644 --- a/lib/Stadium.lua +++ b/lib/Stadium.lua @@ -423,6 +423,15 @@ function Stadium.update(dt, battle, groundY) else ShinyFx.setMetrics(side, nil) end + + -- and let a waiting sparkle GO, once the fight is actually the thing on + -- screen. The battle draws underneath the transition wipe for about a + -- second before that, and a burst released then plays out its whole life + -- behind it -- armed, drawn, counted, and never seen, which is exactly + -- how this looked when it was keyed on the scene drawing instead. + local g = game() + local top = g and g.stack and g.stack:top() + if top == battle then ShinyFx.release(side) end mon.visible = (mon.rig ~= nil) and onField(battle, side, mon) and not (battler and battler.substituteHP) -- LET'S GO capture mode: the player's model is out of the shot the diff --git a/tests/shiny_obs.lua b/tests/shiny_obs.lua index 4c11892..8c29e73 100644 --- a/tests/shiny_obs.lua +++ b/tests/shiny_obs.lua @@ -49,21 +49,25 @@ return function(game) U.log("stadium ready: " .. tostring(StadiumInstall.ready())) OverworldBattle.setting:setValue("stadium", game) - Shiny.setOdds(1) -- every encounter, shiny + -- party FIRST, at ordinary odds: Pokemon.new is where shininess is + -- decided, so pinning the odds before this made the player's own Pikachu + -- shiny -- and that tints the player's side, which during the intro is the + -- trainer sprite. The foe is what these runs are about. game.save.player.name = "RED" game.save.party = { Pokemon.new(game.data, "PIKACHU", 50) } + Shiny.setOdds(1) -- from here on: every encounter + -- Five outdoor places, deliberately unalike -- a coast, an open route, a -- water city, a rocky pass and a wooded shore -- so the recording is five -- different-looking fights and not the same meadow five times. Each mon is -- put somewhere its colour has something to sit against. local RUNS = { - { "BLASTOISE", 45, "PALLET_TOWN", 5, 6 }, - { "PIDGEOTTO", 32, "ROUTE_1", 5, 8 }, - { "GYARADOS", 45, "CERULEAN_CITY", 10, 12 }, { "CHARIZARD", 50, "ROUTE_4", 10, 5 }, - { "NINETALES", 42, "ROUTE_25", 12, 5 }, + { "ELECTRODE", 40, "VIRIDIAN_CITY", 20, 20 }, + { "VAPOREON", 42, "ROUTE_25", 12, 5 }, + { "DRATINI", 30, "ROUTE_3", 10, 5 }, } -- a beat before the first one, so a recorder that started with the window @@ -90,7 +94,7 @@ return function(game) game.overworld:pushBattle(battle) local mon = battle.enemy and battle.enemy.mon - U.log(("%d/5 %-10s %-14s shiny=%s") + U.log(("%d/4 %-10s %-14s shiny=%s") :format(i, species, map, tostring(Shiny.isShiny(mon)))) -- the arrival, then three seconds of nothing but the Pokemon diff --git a/tests/shiny_one.lua b/tests/shiny_one.lua index 7256c02..c0cd678 100644 --- a/tests/shiny_one.lua +++ b/tests/shiny_one.lua @@ -87,11 +87,18 @@ return function(game) end OverworldBattle.setting:setValue("stadium", game) - Shiny.setOdds(1) -- this encounter is shiny + -- THE PARTY IS BUILT FIRST, at ordinary odds, and the roll is only pinned + -- afterwards. Pokemon.new is where shininess is decided, so setting the + -- odds before this line made the player's own Pikachu shiny too -- and a + -- shiny on the player's side tints that side's pic, which during the intro + -- is the TRAINER, so the player sprite came out discoloured for the whole + -- send-out. The foe is the one this run is about. game.save.player.name = "RED" game.save.party = { Pokemon.new(game.data, "PIKACHU", 50) } + Shiny.setOdds(1) -- from here on: the encounter + if not game.data.pokemon[SPECIES] then U.log("no such species: " .. SPECIES) return @@ -121,6 +128,26 @@ return function(game) -- DS_AUTOCLOSE is for checking the pacing without a person in the loop: set -- it to a number of seconds and the run should take about that long by the -- wall clock, which is the only way to prove 1x is actually 1x. + -- DS_SHOTS: capture the ARRIVAL as a strip, for checking that the sparkle + -- fires on its own rather than only when a test arms it by hand. This is + -- the same code path the capture run uses, which is the point -- the last + -- bug here hid precisely because it was verified through a driver that + -- armed the effect itself. + local shots = os.getenv("DS_SHOTS") + if shots then + for k = 1, 26 do + U.shot(game, ("%s/arrive_%02d.png"):format(shots, k)) + hold(0.15) + end + local ShinyFx = lib.require("ShinyFx") + local d = ShinyFx.debug or {} + U.log(("fx: armed=%s cleared=%s calls=%s noArena=%s noLive=%s quads=%s") + :format(tostring(d.armed), tostring(d.cleared), tostring(d.calls), + tostring(d.noArena), tostring(d.noLive), tostring(d.quads))) + U.log("arrival strip written to " .. shots) + return + end + local auto = tonumber(os.getenv("DS_AUTOCLOSE") or "") or 0 if auto > 0 then local t0 = love.timer.getTime() diff --git a/tests/shiny_run.sh b/tests/shiny_run.sh index 9025851..225d927 100644 --- a/tests/shiny_run.sh +++ b/tests/shiny_run.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Five shiny encounters, five outdoor places, one window at a time. +# Shiny encounters, one per outdoor place, one window at a time. # # bash mods/DramaticShapeVoxelMod/tests/shiny_run.sh # @@ -19,12 +19,15 @@ LOVE=${LOVE:-/c/Program Files/LOVE/lovec.exe} DRIVER=mods/DramaticShapeVoxelMod/tests/shiny_one.lua # species | level | map | cell x | cell y +# +# Each in its own outdoor place so no two takes look alike. A species +# or map this dataset does not have is skipped with a line rather than +# failing the run, so trimming this list is just deleting rows. RUNS=( - "BLASTOISE|45|PALLET_TOWN|5|6" - "PIDGEOTTO|32|ROUTE_1|5|8" - "GYARADOS|45|CERULEAN_CITY|10|12" "CHARIZARD|50|ROUTE_4|10|5" - "NINETALES|42|ROUTE_25|12|5" + "ELECTRODE|40|VIRIDIAN_CITY|20|20" + "VAPOREON|42|ROUTE_25|12|5" + "DRATINI|30|ROUTE_3|10|5" ) OPTS="$APPDATA/LOVE/pokemon-love2d/options.lua" @@ -38,7 +41,7 @@ for row in "${RUNS[@]}"; do i=$((i + 1)) IFS='|' read -r SPECIES LEVEL MAP CX CY <<< "$row" echo "" - echo "=== $i/5 $SPECIES at $MAP ===" + echo "=== $i/${#RUNS[@]} $SPECIES at $MAP ===" echo " close the window when you are done recording it" DS_SPECIES="$SPECIES" DS_LEVEL="$LEVEL" DS_MAP="$MAP" \ DS_CX="$CX" DS_CY="$CY" \ @@ -53,4 +56,4 @@ if [ -f "$OPTS.shiny_run_backup" ]; then echo "options.lua restored" fi -echo "done -- five encounters" +echo "done -- ${#RUNS[@]} encounters"