mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 08:01:09 +02:00
the sparkle was playing behind the transition wipe
Armed on the occupant-change edge, which happens while the screen is still mid-wipe -- so the burst spent its whole three-quarter-second life underneath it. The instrumentation is what settled it: armed=1, quads=450, which is 45 frames times 10 stars, exactly LIFE. It was never missing. It was drawing where nobody could see it. Two gates, because the first one was still wrong. Holding the clock until the scene DREW the side does not help: the battle renders under the wipe for about a second before the wipe is gone. The burst now waits until the battle is the top of the stack -- the wipe popped, somebody watching -- and only then starts. Armed and released are separate moments now, which is what they always were. Also, a shiny no longer discolours a PERSON. Both sides can be holding a trainer pic rather than a Pokemon (the foe's portrait before the send-out, the player's own back until "Go!"), and the tint was going straight through it. Shininess is a fact about a Pokemon, not its owner. sideTexture asks the same two questions it already used to label the finished texture. And the capture drivers build the party BEFORE pinning the odds. Pokemon.new is where shininess is decided, so setting odds to 1 first made the player's own Pikachu shiny too -- which tinted the player's side, which during the intro is the trainer sprite. That is what "the player trainer sprite seems discolored" was. Lineup is now Charizard, Electrode, Vaporeon, Dratini. Verified by strip: the burst lands in the frames right after the wipe clears, and the trainer back sprite is its own colour again.
This commit is contained in:
+18
-4
@@ -1108,10 +1108,24 @@ function OverworldBattle.sideTexture(battle, side)
|
|||||||
-- alone, which the engine's both-sides-at-once pic layer cannot do.
|
-- alone, which the engine's both-sides-at-once pic layer cannot do.
|
||||||
local shinyTint = nil
|
local shinyTint = nil
|
||||||
do
|
do
|
||||||
local battler = (side == "player") and battle.player or battle.enemy
|
-- NOT when this side is showing a PERSON. Both sides can be holding a
|
||||||
local g2 = game()
|
-- trainer pic rather than a Pokemon -- the foe's portrait before the
|
||||||
shinyTint = battler and V.require("ShinyUI")
|
-- send-out, and the player's own back until "Go!" -- and a shiny is a
|
||||||
.tintFor(battler.mon, g2 and g2.data) or nil
|
-- 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
|
end
|
||||||
|
|
||||||
local ok, err = pcall(function()
|
local ok, err = pcall(function()
|
||||||
|
|||||||
+41
-3
@@ -163,12 +163,42 @@ end
|
|||||||
-- Start (or restart) the burst on one side. Restarting rather than ignoring
|
-- 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
|
-- a second call is deliberate: a shiny that faints and is sent back out
|
||||||
-- should sparkle again.
|
-- 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)
|
function ShinyFx.arm(side)
|
||||||
if side ~= "player" and side ~= "enemy" then return end
|
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
|
end
|
||||||
|
|
||||||
function ShinyFx.clear(side)
|
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
|
if side then live[side] = nil else live.player, live.enemy = nil, nil end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -181,7 +211,9 @@ function ShinyFx.update(dt)
|
|||||||
dt = dt or 0
|
dt = dt or 0
|
||||||
for _, side in ipairs({ "player", "enemy" }) do
|
for _, side in ipairs({ "player", "enemy" }) do
|
||||||
local s = live[side]
|
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
|
s.t = s.t + dt
|
||||||
if s.t >= ShinyFx.LIFE then live[side] = nil end
|
if s.t >= ShinyFx.LIFE then live[side] = nil end
|
||||||
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
|
-- invisible to the test suite and this one has four separate ways to be a
|
||||||
-- no-op, all of them silent.
|
-- no-op, all of them silent.
|
||||||
ShinyFx.debug = { calls = 0, noArena = 0, noImage = 0, noMesh = 0,
|
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)
|
function ShinyFx.draw(arena, groundY, pull)
|
||||||
local dbg = ShinyFx.debug
|
local dbg = ShinyFx.debug
|
||||||
@@ -220,6 +252,12 @@ function ShinyFx.draw(arena, groundY, pull)
|
|||||||
for _, side in ipairs({ "player", "enemy" }) do
|
for _, side in ipairs({ "player", "enemy" }) do
|
||||||
local s = live[side]
|
local s = live[side]
|
||||||
local cell = (side == "player") and arena.player or arena.enemy
|
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
|
if s and cell then
|
||||||
local u = math.min(1, s.t / ShinyFx.LIFE)
|
local u = math.min(1, s.t / ShinyFx.LIFE)
|
||||||
local e = easeOut(u)
|
local e = easeOut(u)
|
||||||
|
|||||||
@@ -423,6 +423,15 @@ function Stadium.update(dt, battle, groundY)
|
|||||||
else
|
else
|
||||||
ShinyFx.setMetrics(side, nil)
|
ShinyFx.setMetrics(side, nil)
|
||||||
end
|
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)
|
mon.visible = (mon.rig ~= nil) and onField(battle, side, mon)
|
||||||
and not (battler and battler.substituteHP)
|
and not (battler and battler.substituteHP)
|
||||||
-- LET'S GO capture mode: the player's model is out of the shot the
|
-- LET'S GO capture mode: the player's model is out of the shot the
|
||||||
|
|||||||
+10
-6
@@ -49,21 +49,25 @@ return function(game)
|
|||||||
U.log("stadium ready: " .. tostring(StadiumInstall.ready()))
|
U.log("stadium ready: " .. tostring(StadiumInstall.ready()))
|
||||||
|
|
||||||
OverworldBattle.setting:setValue("stadium", game)
|
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.player.name = "RED"
|
||||||
game.save.party = { Pokemon.new(game.data, "PIKACHU", 50) }
|
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
|
-- 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
|
-- 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
|
-- different-looking fights and not the same meadow five times. Each mon is
|
||||||
-- put somewhere its colour has something to sit against.
|
-- put somewhere its colour has something to sit against.
|
||||||
local RUNS = {
|
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 },
|
{ "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
|
-- 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)
|
game.overworld:pushBattle(battle)
|
||||||
|
|
||||||
local mon = battle.enemy and battle.enemy.mon
|
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))))
|
:format(i, species, map, tostring(Shiny.isShiny(mon))))
|
||||||
|
|
||||||
-- the arrival, then three seconds of nothing but the Pokemon
|
-- the arrival, then three seconds of nothing but the Pokemon
|
||||||
|
|||||||
+28
-1
@@ -87,11 +87,18 @@ return function(game)
|
|||||||
end
|
end
|
||||||
|
|
||||||
OverworldBattle.setting:setValue("stadium", game)
|
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.player.name = "RED"
|
||||||
game.save.party = { Pokemon.new(game.data, "PIKACHU", 50) }
|
game.save.party = { Pokemon.new(game.data, "PIKACHU", 50) }
|
||||||
|
|
||||||
|
Shiny.setOdds(1) -- from here on: the encounter
|
||||||
|
|
||||||
if not game.data.pokemon[SPECIES] then
|
if not game.data.pokemon[SPECIES] then
|
||||||
U.log("no such species: " .. SPECIES)
|
U.log("no such species: " .. SPECIES)
|
||||||
return
|
return
|
||||||
@@ -121,6 +128,26 @@ return function(game)
|
|||||||
-- DS_AUTOCLOSE is for checking the pacing without a person in the loop: set
|
-- 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
|
-- 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.
|
-- 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
|
local auto = tonumber(os.getenv("DS_AUTOCLOSE") or "") or 0
|
||||||
if auto > 0 then
|
if auto > 0 then
|
||||||
local t0 = love.timer.getTime()
|
local t0 = love.timer.getTime()
|
||||||
|
|||||||
+10
-7
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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
|
# 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
|
DRIVER=mods/DramaticShapeVoxelMod/tests/shiny_one.lua
|
||||||
|
|
||||||
# species | level | map | cell x | cell y
|
# 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=(
|
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"
|
"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"
|
OPTS="$APPDATA/LOVE/pokemon-love2d/options.lua"
|
||||||
@@ -38,7 +41,7 @@ for row in "${RUNS[@]}"; do
|
|||||||
i=$((i + 1))
|
i=$((i + 1))
|
||||||
IFS='|' read -r SPECIES LEVEL MAP CX CY <<< "$row"
|
IFS='|' read -r SPECIES LEVEL MAP CX CY <<< "$row"
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== $i/5 $SPECIES at $MAP ==="
|
echo "=== $i/${#RUNS[@]} $SPECIES at $MAP ==="
|
||||||
echo " close the window when you are done recording it"
|
echo " close the window when you are done recording it"
|
||||||
DS_SPECIES="$SPECIES" DS_LEVEL="$LEVEL" DS_MAP="$MAP" \
|
DS_SPECIES="$SPECIES" DS_LEVEL="$LEVEL" DS_MAP="$MAP" \
|
||||||
DS_CX="$CX" DS_CY="$CY" \
|
DS_CX="$CX" DS_CY="$CY" \
|
||||||
@@ -53,4 +56,4 @@ if [ -f "$OPTS.shiny_run_backup" ]; then
|
|||||||
echo "options.lua restored"
|
echo "options.lua restored"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "done -- five encounters"
|
echo "done -- ${#RUNS[@]} encounters"
|
||||||
|
|||||||
Reference in New Issue
Block a user