remove fireflies
This commit is contained in:
+11
-51
@@ -4,57 +4,6 @@
|
||||
|
||||
### Added
|
||||
|
||||
- **Fireflies over tall grass, on every outdoor map.** Viridian Forest has
|
||||
had them since 1.6.2 -- blinking green motes low in the air, dealt over
|
||||
the forest's whole volume as part of its atmosphere. They now come out
|
||||
over TALL GRASS anywhere the hour reaches: a route at midnight has
|
||||
lights loose over its grass and none over the road beside it.
|
||||
|
||||
The same particle, moved rather than rebuilt. Same mesh format, same
|
||||
blinking shader, same one ramp off the day/night clock -- so they come
|
||||
on through dusk and reach full contrast at deepest night exactly when
|
||||
the forest's do, because it is the same function answering (pulled out
|
||||
of `ForestAtmos.frame` as `fireflyLevel` so the two cannot drift
|
||||
apart). What changed is where they are dealt.
|
||||
|
||||
**Grass is the entry.** `data/map_atmosphere.lua` is still the opt-in
|
||||
for fog and god rays, and still names one map; the grass fireflies need
|
||||
no line in it. Each map is scanned once for the cells the engine's own
|
||||
`isGrassCell` calls tall grass -- the collision tile, the rule that
|
||||
decides where a wild battle starts, and the same test `Structures`
|
||||
passes before it sprouts a tuft. Going by the grass GRAPHIC instead
|
||||
would hang lights over town plazas, where the same art turns up as
|
||||
decorative filler inside ordinary ground blocks. Roughly four fireflies
|
||||
to five grass cells, capped at 200 a map; an atmosphere entry may tune
|
||||
both through a `grassFlies` row. A map with no grass on it deals none
|
||||
and costs one scan.
|
||||
|
||||
They are dealt INTO a cell rather than at its middle, and the shader's
|
||||
existing wander carries each about a cell's width from where it was
|
||||
dealt, in the tufts' own height band -- so a patch reads as a patch
|
||||
with fireflies loose over it rather than as a grid of lights. The deal
|
||||
is seeded off the map id, so a route's swarm is the same arrangement on
|
||||
every visit and no two routes get the same one.
|
||||
|
||||
**The connected neighbours get theirs too.** A neighbour map is drawn
|
||||
in full -- ground, trees, grass and the firefly cards standing in it --
|
||||
so a swarm that stopped at the map seam would draw a line across the
|
||||
picture where the lights ran out. The particle shader takes a map
|
||||
origin now and each neighbour's own cached mesh is drawn where its
|
||||
terrain is. Nothing else in the atmosphere crosses a seam, and nothing
|
||||
else needed to: fog and beams belong to the map you are standing in.
|
||||
|
||||
This is the FULL rung of FOREST FX, like every particle in that pass.
|
||||
The per-tuft firefly cards in the grass mesh are the layer underneath
|
||||
and are unchanged -- static geometry the scene shader already carries,
|
||||
so grass still has something alight after dark on LOW and on Android,
|
||||
where this pass is not drawn at all.
|
||||
|
||||
One fix falls out of it: a machine that cannot hand back a readable
|
||||
depth buffer used to lose the whole atmosphere pass, particles
|
||||
included, rather than just the beams it actually blocks. The refusal
|
||||
now takes the beams alone.
|
||||
|
||||
- **SHINY POKEMON.** On by default, with no row to switch it off:
|
||||
shininess is a property of a Pokemon, not a display mode, and one that
|
||||
differed between two players' saves would be a setting rather than a
|
||||
@@ -127,6 +76,17 @@
|
||||
pixel rather than floating over the finished frame. Page 1 only: page 2
|
||||
clears that block itself.
|
||||
|
||||
**SHINY ODDS**, on the DRAMATIC SHAPE menu itself rather than in one of
|
||||
its four categories -- those are the diorama, the fights, what the look
|
||||
costs and the headset, and an encounter rate is none of them. The row
|
||||
reads `1:8192` and halves down to `1:1`, so every rung is exactly twice
|
||||
as often as the one above it, and `1:8192` is both the default and the
|
||||
fallback for an unreadable options file: the mod's default is the games'
|
||||
own rate, not a buff. The number is the truth rather than an
|
||||
approximation of it, because a missed roll also clears a mon that landed
|
||||
on the pattern by luck -- without that, every setting would be itself
|
||||
and 1/8192 in parallel, and no setting could ever be rarer than 8192.
|
||||
|
||||
## 1.8.0
|
||||
|
||||
### Added
|
||||
|
||||
+2
-14
@@ -13,9 +13,7 @@
|
||||
-- light a canopy map ever gets -- see DayNight.CANOPY); only its COLOUR
|
||||
-- and STRENGTH follow the clock: gold spears of sun by day, silver moon
|
||||
-- rays after dark, pollen adrift in the day's beams and fireflies once
|
||||
-- they cool. The fireflies are the one part of this that reaches past the
|
||||
-- table: they also come out over TALL GRASS on every outdoor map, entry
|
||||
-- or no entry (see grassFlies below).
|
||||
-- they cool.
|
||||
--
|
||||
-- A map with no entry here has no atmosphere at all: no fog uniform is
|
||||
-- raised, no march runs, nothing is spent. That is the contract a new
|
||||
@@ -38,18 +36,8 @@
|
||||
-- rays strength overall in-scatter gain on the march
|
||||
-- reach how far out the march walks, in world px
|
||||
-- motes count of pollen/dust flecks adrift in the daylight beams
|
||||
-- fireflies count of the night shift, dealt over the map's whole volume
|
||||
-- fireflies count of the night shift
|
||||
-- seed the xorshift seed the particle deal runs on
|
||||
-- grassFlies per fireflies dealt per TALL GRASS cell (default 0.8)
|
||||
-- cap and the most one map may have of them (default 200)
|
||||
--
|
||||
-- That last row is the odd one out, and the only knob here a map does not
|
||||
-- need an entry to get. The GRASS fireflies are dealt over a map's tall
|
||||
-- grass rather than over its air, and every outdoor map has them -- grass
|
||||
-- is the entry, so a route gets fireflies after dark without a line here
|
||||
-- and a map with no grass on it deals none. What an entry buys is TUNING:
|
||||
-- a `grassFlies` row moves the two numbers off their defaults, and that
|
||||
-- is the whole of it. See ForestAtmos, under "the grass fireflies".
|
||||
--
|
||||
-- Two caveats for maps opting in later: the water pass has no fog term,
|
||||
-- so a lake under heavy haze stays clear-day sharp in its reflections;
|
||||
|
||||
+21
-183
@@ -137,7 +137,6 @@ ForestAtmos.configFor = configFor
|
||||
-- shader and texture sentinels, which is what a lost GL context needs.
|
||||
|
||||
local layoutCache = {}
|
||||
local grassCache = {} -- the grass deal, keyed by map; false = no grass
|
||||
local meshCache = {}
|
||||
local shaders = {} -- keyed by variant; nil untried, false refused
|
||||
local leafTex = nil -- the tiling leaf-dapple field
|
||||
@@ -157,10 +156,9 @@ end
|
||||
function ForestAtmos.invalidate(mapId)
|
||||
if mapId then
|
||||
layoutCache[mapId] = nil
|
||||
grassCache[mapId] = nil
|
||||
meshCache[mapId] = nil
|
||||
else
|
||||
layoutCache, grassCache, meshCache = {}, {}, {}
|
||||
layoutCache, meshCache = {}, {}
|
||||
shaders = {}
|
||||
leafTex = nil
|
||||
rayMesh = nil
|
||||
@@ -196,18 +194,6 @@ ForestAtmos.RAMP = {
|
||||
alpha = 0.40, density = 1.15, motes = 0.0, flies = 1.0 },
|
||||
}
|
||||
|
||||
-- How far out the night shift is, 0..1, at clock `t`. Pulled out of the
|
||||
-- frame's own loop because the GRASS fireflies (below) need it on maps
|
||||
-- that have no atmosphere at all -- and pulled out rather than copied so
|
||||
-- the two can never disagree about when a firefly comes on.
|
||||
function ForestAtmos.fireflyLevel(t)
|
||||
local flies = 0
|
||||
for name, w in pairs(DayNight.mix(t or DayNight.time())) do
|
||||
flies = flies + (ForestAtmos.RAMP[name] or ForestAtmos.RAMP.day).flies * w
|
||||
end
|
||||
return flies
|
||||
end
|
||||
|
||||
-- The frame's atmosphere for `map` at clock `t` (defaulting to now), or
|
||||
-- nil -- no entry, or the row is OFF -- in which case nothing is drawn
|
||||
-- and Voxel3D.fog should be left nil.
|
||||
@@ -217,8 +203,7 @@ function ForestAtmos.frame(map, t)
|
||||
if not cfg then return nil end
|
||||
local mix = DayNight.mix(t or DayNight.time())
|
||||
local fr, fg, fb, rr, rg, rb = 0, 0, 0, 0, 0, 0
|
||||
local alpha, dens, motes = 0, 0, 0
|
||||
local flies = ForestAtmos.fireflyLevel(t)
|
||||
local alpha, dens, motes, flies = 0, 0, 0, 0
|
||||
for name, w in pairs(mix) do
|
||||
local p = ForestAtmos.RAMP[name] or ForestAtmos.RAMP.day
|
||||
fr, fg, fb = fr + p.fog[1] * w, fg + p.fog[2] * w, fb + p.fog[3] * w
|
||||
@@ -226,6 +211,7 @@ function ForestAtmos.frame(map, t)
|
||||
alpha = alpha + p.alpha * w
|
||||
dens = dens + p.density * w
|
||||
motes = motes + p.motes * w
|
||||
flies = flies + p.flies * w
|
||||
end
|
||||
-- the haze takes on a little of the light standing in it
|
||||
local LEAN = 0.15
|
||||
@@ -388,111 +374,6 @@ function ForestAtmos.layout(cfg, w, h)
|
||||
return { motes = motes, flies = flies }
|
||||
end
|
||||
|
||||
-- ------- the grass fireflies
|
||||
--
|
||||
-- The same particle -- same mesh format, same blinking shader, same hour's
|
||||
-- ramp -- dealt over a map's TALL GRASS instead of over its whole volume,
|
||||
-- and on any outdoor map rather than only the ones with an atmosphere
|
||||
-- entry. Grass IS the entry: a route with tall grass on it gets fireflies
|
||||
-- after dark without anybody authoring a line, and a map with no grass
|
||||
-- cell on it deals nothing and costs one scan.
|
||||
--
|
||||
-- Placement asks the engine's own question, `map:isGrassCell` -- the
|
||||
-- cell's collision tile, the rule that decides where a wild battle can
|
||||
-- start -- which is the same test Structures runs before it sprouts a
|
||||
-- tuft. The grass GRAPHIC also turns up as decorative filler inside plain
|
||||
-- ground blocks, and going by the tile would hang fireflies over town
|
||||
-- plazas (the trap that note in Structures.buildGrass records).
|
||||
--
|
||||
-- They are dealt into the cell rather than at its middle, and the shader's
|
||||
-- own wander (sway 10, 4) carries each one about a cell's width from where
|
||||
-- it was dealt -- so a patch reads as a patch with fireflies loose over it,
|
||||
-- not as a grid of lights. The height band is the tufts' own: 3..15, which
|
||||
-- is blade height and a little air (Structures stands its blades to y = 16).
|
||||
--
|
||||
-- This is the FULL rung, like every other particle here. The per-tuft
|
||||
-- firefly cards in Structures.buildGrass are the layer underneath -- they
|
||||
-- are static geometry the scene shader already carries, so grass still has
|
||||
-- something alight after dark on LOW and on Android, where this pass is
|
||||
-- not drawn at all.
|
||||
local GRASS_PER_CELL = 0.8 -- fireflies per grass cell...
|
||||
local GRASS_CAP = 200 -- ...up to this many on one map
|
||||
local CELL = 16 -- world px, the grid isGrassCell speaks
|
||||
|
||||
-- A map id deals its own swarm: stable across visits and saves (nothing
|
||||
-- here rides the clock), different between maps, so two routes with the
|
||||
-- same amount of grass do not get the same arrangement of lights.
|
||||
local function seedOf(id)
|
||||
local s = 0x51D
|
||||
for i = 1, #id do s = (s * 31 + id:byte(i)) % 0x100000000 end
|
||||
return s
|
||||
end
|
||||
|
||||
-- `cells` is a flat list of { cx, cy } pairs; count is worked out from how
|
||||
-- many there are. Deterministic, and separated from the map so a test can
|
||||
-- hand it a meadow.
|
||||
function ForestAtmos.grassLayout(cells, seed, per, cap)
|
||||
local n = #cells
|
||||
if n == 0 then return {} end
|
||||
local want = min(floor(n * (per or GRASS_PER_CELL) + 0.5), cap or GRASS_CAP)
|
||||
local rng = newRng(seed or 0x51D)
|
||||
local flies = {}
|
||||
for _ = 1, want do
|
||||
local c = cells[1 + min(floor(rng:unit() * n), n - 1)]
|
||||
flies[#flies + 1] = {
|
||||
x = c[1] * CELL + rng:unit() * CELL,
|
||||
y = 3 + rng:unit() * 12,
|
||||
z = c[2] * CELL + rng:unit() * CELL,
|
||||
phase = rng:unit() * 6.2832,
|
||||
rate = 0.5 + rng:unit(),
|
||||
}
|
||||
end
|
||||
return flies
|
||||
end
|
||||
|
||||
-- Only where the clock reaches: an outdoor map, or a CANOPY one (Viridian
|
||||
-- Forest is not outdoor -- no sky, no sun -- but night still falls in it,
|
||||
-- and it is the map these fireflies were drawn for). A cave stays a cave.
|
||||
local function litByTheHour(map)
|
||||
if DayNight.isCanopy(map) then return true end
|
||||
local ok, Map = pcall(require, "src.world.Map")
|
||||
if not (ok and Map and Map.isOutdoor) then return false end
|
||||
local got, outdoor = pcall(Map.isOutdoor, map.def or {})
|
||||
return got and outdoor or false
|
||||
end
|
||||
|
||||
-- One scan of the map's cells, cached with everything else that goes stale
|
||||
-- with it. `false` records "scanned, no grass" so a grassless map is never
|
||||
-- walked twice.
|
||||
local function grassFliesFor(map)
|
||||
local hit = grassCache[map.id]
|
||||
if hit ~= nil then return hit or nil end
|
||||
if not (map.isGrassCell and litByTheHour(map)) then
|
||||
grassCache[map.id] = false
|
||||
return nil
|
||||
end
|
||||
local cells = {}
|
||||
local w = map.widthCells or ((map.def and map.def.width or 0) * 2)
|
||||
local h = map.heightCells or ((map.def and map.def.height or 0) * 2)
|
||||
-- one pcall around the whole scan, not one per cell: a route is a couple
|
||||
-- of thousand cells and this runs on the frame that first draws the map
|
||||
pcall(function()
|
||||
for cy = 0, h - 1 do
|
||||
for cx = 0, w - 1 do
|
||||
if map:isGrassCell(cx, cy) then cells[#cells + 1] = { cx, cy } end
|
||||
end
|
||||
end
|
||||
end)
|
||||
local cfg = configFor(map.id) or {}
|
||||
local knob = cfg.grassFlies or {}
|
||||
local flies = ForestAtmos.grassLayout(cells, seedOf(map.id),
|
||||
knob.per, knob.cap)
|
||||
grassCache[map.id] = (#flies > 0) and flies or false
|
||||
return grassCache[map.id] or nil
|
||||
end
|
||||
|
||||
ForestAtmos.grassFliesFor = grassFliesFor
|
||||
|
||||
-- A map is width x height BLOCKS of 4x4 tiles of 8 pixels -- times 32
|
||||
-- for world pixels (the same arithmetic Structures runs in tiles).
|
||||
local function layoutFor(map)
|
||||
@@ -756,9 +637,6 @@ local PART_SHADER = [[
|
||||
uniform float size;
|
||||
uniform vec2 sway; // wander amplitude: horizontal, vertical
|
||||
uniform float blinky; // 0 = steady motes, 1 = blinking fireflies
|
||||
uniform vec3 origin; // the map's corner: zero for the one being
|
||||
// stood on, the connection offset for a
|
||||
// neighbour's swarm (one mesh, drawn per map)
|
||||
attribute vec4 AtmosData; // corner x, corner y, phase, rate
|
||||
vec4 position(mat4 transform_projection, vec4 vertex_position) {
|
||||
float ph = AtmosData.z;
|
||||
@@ -766,7 +644,7 @@ local PART_SHADER = [[
|
||||
float t = time * (0.5 + rt);
|
||||
// bounded wander only -- three incommensurate sines, so nothing ever
|
||||
// walks off the map or needs a CPU tick to bring it home
|
||||
vec3 base = vertex_position.xyz + origin + vec3(
|
||||
vec3 base = vertex_position.xyz + vec3(
|
||||
sin(t * 0.23 + ph) * sway.x,
|
||||
sin(t * 0.17 + ph * 2.7) * sway.y,
|
||||
cos(t * 0.19 + ph * 1.3) * sway.x);
|
||||
@@ -855,19 +733,12 @@ local function buildPartMesh(points)
|
||||
return mesh
|
||||
end
|
||||
|
||||
-- The map's three swarms in one cache entry: the atmosphere's pollen and
|
||||
-- fireflies, which exist only for a map with an entry, and the grass
|
||||
-- fireflies, which exist for any outdoor map with tall grass on it. Each
|
||||
-- is nil on its own -- a route builds one mesh, Viridian Forest builds
|
||||
-- three, and a cave builds none.
|
||||
local function meshesFor(map)
|
||||
local function meshesFor(map, L)
|
||||
local hit = meshCache[map.id]
|
||||
if hit then return hit end
|
||||
local L = layoutFor(map)
|
||||
local M = {
|
||||
motes = L and buildPartMesh(L.motes) or nil,
|
||||
flies = L and buildPartMesh(L.flies) or nil,
|
||||
grass = buildPartMesh(grassFliesFor(map) or {}),
|
||||
motes = buildPartMesh(L.motes),
|
||||
flies = buildPartMesh(L.flies),
|
||||
}
|
||||
meshCache[map.id] = M
|
||||
return M
|
||||
@@ -875,7 +746,6 @@ end
|
||||
|
||||
local MOTE_COLOR = { 1.0, 0.96, 0.78 }
|
||||
local FLY_COLOR = { 0.72, 1.0, 0.45 }
|
||||
local HOME = { 0, 0, 0 } -- the origin of the map being stood on
|
||||
|
||||
-- The billboard frame: the camera's own right and up, from the same
|
||||
-- fields every pass sets (per VR eye too -- drawScene runs per eye and
|
||||
@@ -904,29 +774,19 @@ end
|
||||
-- geometry like the Stadium flames. Anything missing -- no entry, OFF, a
|
||||
-- refused shader, no readable depth, no shadow map -- subtracts only
|
||||
-- itself.
|
||||
--
|
||||
-- `f` is nil for every map without an atmosphere entry, and the pass does
|
||||
-- NOT stop there any more: the grass fireflies belong to the grass, not to
|
||||
-- an authored line, so the fog and the beams sit that map out and the
|
||||
-- particle block below still runs. Every read of `f` past here is guarded.
|
||||
function ForestAtmos.draw(map, neighbors)
|
||||
function ForestAtmos.draw(map)
|
||||
local rung = ForestAtmos.setting:get()
|
||||
if rung == "off" then return end
|
||||
if not (map and map.id) then return end
|
||||
local f = ForestAtmos.frame(map)
|
||||
if not f then return end
|
||||
local Voxel3D = V.require("Voxel3D")
|
||||
local ShadowMap = V.require("ShadowMap")
|
||||
|
||||
-- A refusal here takes the BEAMS, not the frame: this one used to
|
||||
-- `return` out of the function, which took the particles with it. They
|
||||
-- fall through now, so a machine that cannot hand back a readable depth
|
||||
-- buffer still gets fireflies over its grass.
|
||||
local beams = f and f.rayAlpha > 0.01 or false
|
||||
if beams and not Voxel3D.depthReadable() then
|
||||
say("depth", "no readable depth this frame -- beams off, fog stays")
|
||||
beams = false
|
||||
end
|
||||
if beams then
|
||||
if f.rayAlpha > 0.01 then
|
||||
if not Voxel3D.depthReadable() then
|
||||
say("depth", "no readable depth this frame -- beams off, fog stays")
|
||||
return
|
||||
end
|
||||
-- no beams without the sun's own pass: uvVP is only the world -> map
|
||||
-- transform while a shadow map is actually standing
|
||||
local sunTex = ShadowMap.active() and ShadowMap.texture()
|
||||
@@ -983,13 +843,11 @@ function ForestAtmos.draw(map, neighbors)
|
||||
end
|
||||
|
||||
if rung == "full" then
|
||||
local M = meshesFor(map)
|
||||
-- the night shift, for the grass swarm: the atmosphere's own answer
|
||||
-- where there is one, and the bare ramp where there is not
|
||||
local flyLevel = f and f.fireflyLevel or ForestAtmos.fireflyLevel()
|
||||
local L = layoutFor(map)
|
||||
local M = L and meshesFor(map, L)
|
||||
local psh = partShader()
|
||||
local axisR, axisU = billboardAxes(Voxel3D)
|
||||
if (M.motes or M.flies or M.grass) and psh and axisR then
|
||||
if M and psh and axisR then
|
||||
Voxel3D.blend("add")
|
||||
if Voxel3D.beginEffect(psh) then
|
||||
pcall(psh.send, psh, "vp", "row", Voxel3D.vp)
|
||||
@@ -1002,8 +860,7 @@ function ForestAtmos.draw(map, neighbors)
|
||||
pcall(psh.send, psh, "axisR", axisR)
|
||||
pcall(psh.send, psh, "axisU", axisU)
|
||||
pcall(psh.send, psh, "time", ForestAtmos.time)
|
||||
pcall(psh.send, psh, "origin", HOME)
|
||||
if M.motes and f and f.moteLevel > 0.02 then
|
||||
if M.motes and f.moteLevel > 0.02 then
|
||||
pcall(psh.send, psh, "size", 1.4)
|
||||
pcall(psh.send, psh, "sway", { 5, 2.5 })
|
||||
pcall(psh.send, psh, "blinky", 0)
|
||||
@@ -1011,32 +868,13 @@ function ForestAtmos.draw(map, neighbors)
|
||||
pcall(psh.send, psh, "level", f.moteLevel * 0.5)
|
||||
pcall(love.graphics.draw, M.motes)
|
||||
end
|
||||
-- every swarm of fireflies shares every uniform but the mesh and
|
||||
-- the map corner it stands on: the grass ones ARE the forest's,
|
||||
-- moved onto the grass.
|
||||
--
|
||||
-- The NEIGHBOURS matter here in a way they never did for the fog
|
||||
-- or the beams. A connected route is drawn in full -- its ground,
|
||||
-- its trees, its grass and the firefly cards standing in it -- so
|
||||
-- a swarm that stopped at the seam would draw a line across the
|
||||
-- map where the lights ran out. Each neighbour's own deal is a
|
||||
-- mesh already cached against its id; it costs a uniform and a
|
||||
-- draw call to put it where the terrain under it is.
|
||||
if flyLevel > 0.02 then
|
||||
if M.flies and f.fireflyLevel > 0.02 then
|
||||
pcall(psh.send, psh, "size", 1.6)
|
||||
pcall(psh.send, psh, "sway", { 10, 4 })
|
||||
pcall(psh.send, psh, "blinky", 1)
|
||||
pcall(psh.send, psh, "dotColor", FLY_COLOR)
|
||||
pcall(psh.send, psh, "level", flyLevel * 0.85)
|
||||
if M.flies then pcall(love.graphics.draw, M.flies) end
|
||||
if M.grass then pcall(love.graphics.draw, M.grass) end
|
||||
for _, nb in ipairs(neighbors or {}) do
|
||||
local NM = nb.map and nb.map.id and meshesFor(nb.map)
|
||||
if NM and NM.grass then
|
||||
pcall(psh.send, psh, "origin", { nb.ox or 0, 0, nb.oy or 0 })
|
||||
pcall(love.graphics.draw, NM.grass)
|
||||
end
|
||||
end
|
||||
pcall(psh.send, psh, "level", f.fireflyLevel * 0.85)
|
||||
pcall(love.graphics.draw, M.flies)
|
||||
end
|
||||
Voxel3D.endEffect()
|
||||
end
|
||||
|
||||
@@ -192,6 +192,22 @@ function SettingsMenu.rows(catId, game)
|
||||
out[#out + 1] = row
|
||||
end
|
||||
end
|
||||
-- ------- settings that belong to no category
|
||||
--
|
||||
-- A row can name SettingsMenu.ROOT as its `cat` and sit on the top-level
|
||||
-- menu next to the pipeline rows. For a setting that is about the GAME
|
||||
-- rather than about one of the four things the categories are for --
|
||||
-- SHINY ODDS is the first -- burying it under a heading it does not
|
||||
-- belong to is worse than the flat list this menu was built to end.
|
||||
--
|
||||
-- Above the categories, because these are rows you CHANGE and those are
|
||||
-- rows you OPEN: everything with a value on it stays together at the top
|
||||
-- of the screen, and the "..." rows read as the way further in.
|
||||
for _, entry in ipairs(settings) do
|
||||
if entry.cat == SettingsMenu.ROOT and offered(entry, full) then
|
||||
out[#out + 1] = entry[1]:row()
|
||||
end
|
||||
end
|
||||
for _, cat in ipairs(SettingsMenu.CATEGORIES) do
|
||||
local kids = SettingsMenu.rows(cat.id, game)
|
||||
-- An EMPTY category is not offered. This is the whole of what makes
|
||||
|
||||
+65
-5
@@ -42,6 +42,8 @@ local V = ...
|
||||
-- src.pokemon.Stats precisely so an indicator mod can call isShiny
|
||||
local Stats = require("src.pokemon.Stats")
|
||||
|
||||
local ModSetting = V.require("ModSetting")
|
||||
|
||||
local Shiny = {}
|
||||
|
||||
-- ------- the odds
|
||||
@@ -59,17 +61,75 @@ local Shiny = {}
|
||||
-- the number says.
|
||||
Shiny.ODDS_DENOM = 8192
|
||||
|
||||
-- Set the denominator. Guards the degenerate values because a 0 or a
|
||||
-- negative here would divide-by-zero or make every encounter shiny by
|
||||
-- accident rather than by choice; 1 (always shiny) stays reachable because
|
||||
-- it is genuinely useful for walking the whole model set.
|
||||
-- ------- the row the player cycles
|
||||
--
|
||||
-- A ladder that HALVES, so every step is exactly "twice as often as the one
|
||||
-- above it" and the label says the whole truth -- 1:8192 down to 1:1. The
|
||||
-- rate is what the number says, not an approximation of it, because the
|
||||
-- miss branch of decide() closes the natural 1/8192 (see above); a rung of
|
||||
-- 1:2 really is every other encounter.
|
||||
--
|
||||
-- values[1] is 8192: ModSetting treats the first rung as both the DEFAULT
|
||||
-- and the fallback for an unreadable or unrecognised stored value, so the
|
||||
-- canonical rate is what a player who never opens the menu gets and what a
|
||||
-- corrupted options.lua comes back to.
|
||||
--
|
||||
-- No rung RARER than 8192. The mod's promise is that its default is not a
|
||||
-- change to the game; making the game harder than it ships is a different
|
||||
-- promise and nobody asked for it.
|
||||
local ODDS = { 8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1 }
|
||||
|
||||
local ODDS_LABELS = {}
|
||||
for i, n in ipairs(ODDS) do ODDS_LABELS[i] = "1:" .. n end
|
||||
|
||||
Shiny.setting = ModSetting.new("shinyOdds", "SHINY ODDS", ODDS, ODDS_LABELS)
|
||||
|
||||
-- ------- the setting is PULLED, not pushed
|
||||
--
|
||||
-- decide() asks this every roll rather than the menu telling us when it
|
||||
-- changed. Two writers exist -- the OPTIONS row and the mod manager's own
|
||||
-- settings page -- and only the first has a change hook to hang on; the
|
||||
-- manager writes through mod.options and calls ModSetting:sync, which
|
||||
-- notifies nothing. Pulling is the only way both are seen, and the cost is
|
||||
-- a table read on an event that happens once per encounter.
|
||||
--
|
||||
-- ODDS_DENOM stays the live value and is written through on every ask, so
|
||||
-- anything already reading that field keeps reading the truth.
|
||||
local pinned = false
|
||||
|
||||
function Shiny.odds()
|
||||
if not pinned then
|
||||
local ok, value = pcall(Shiny.setting.get, Shiny.setting)
|
||||
local n = ok and tonumber(value)
|
||||
if n and n >= 1 then Shiny.ODDS_DENOM = math.floor(n) end
|
||||
end
|
||||
return Shiny.ODDS_DENOM
|
||||
end
|
||||
|
||||
-- Set the denominator BY HAND, which also PINS it: a driver or a test that
|
||||
-- has asked for 1:1 means it, and must not have the next roll quietly put
|
||||
-- back to whatever the player left on the menu. Nothing in the game calls
|
||||
-- this -- the row is how a player changes the rate.
|
||||
--
|
||||
-- Guards the degenerate values because a 0 or a negative here would
|
||||
-- divide-by-zero or make every encounter shiny by accident rather than by
|
||||
-- choice; 1 (always shiny) stays reachable because it is genuinely useful
|
||||
-- for walking the whole model set.
|
||||
function Shiny.setOdds(denom)
|
||||
denom = tonumber(denom)
|
||||
if not denom or denom < 1 then return Shiny.ODDS_DENOM end
|
||||
Shiny.ODDS_DENOM = math.floor(denom)
|
||||
pinned = true
|
||||
return Shiny.ODDS_DENOM
|
||||
end
|
||||
|
||||
-- Hand the row back control, for a test that pinned the odds and wants the
|
||||
-- setting to mean something again afterwards.
|
||||
function Shiny.unpinOdds()
|
||||
pinned = false
|
||||
return Shiny.odds()
|
||||
end
|
||||
|
||||
-- ------- reading it
|
||||
|
||||
-- The eight Attack DVs that satisfy the Gen 2 pattern, in order. Kept as a
|
||||
@@ -205,7 +265,7 @@ function Shiny.decide(mon, rng)
|
||||
-- same shape as love.math.random(lo, hi), so a caller can pass that or a
|
||||
-- stub and the call below reads identically either way
|
||||
rng = rng or function(_lo, hi) return roll(hi) end
|
||||
local hit = rng(1, Shiny.ODDS_DENOM) == 1
|
||||
local hit = rng(1, Shiny.odds()) == 1
|
||||
|
||||
if hit then
|
||||
forceShiny(mon.dvs)
|
||||
|
||||
+2
-4
@@ -1246,10 +1246,8 @@ function VoxelScene.render(state, w, h, vw, vh, paletteFor, eyes)
|
||||
-- finished depth buffer, so the trees occlude the light and the light
|
||||
-- writes nothing; here in the prop slot, after everything the beams
|
||||
-- should fall across and inside drawScene so VR gets them per eye. On
|
||||
-- the one map that has any, today -- but the FIREFLIES over tall grass
|
||||
-- need no entry and reach every outdoor map, which is why the connected
|
||||
-- neighbours go too: their grass is drawn, so their lights are.
|
||||
ForestAtmos.draw(state.map, state.neighbors)
|
||||
-- the one map that has any, today.
|
||||
ForestAtmos.draw(state.map)
|
||||
|
||||
-- The VR pokedex in the player's left hand, last of all: a prop over
|
||||
-- the world drawn with real depth, so leaning it into a wall still
|
||||
|
||||
@@ -88,6 +88,7 @@ local WorldCurve = V.require("WorldCurve")
|
||||
local ViewBox = V.require("ViewBox")
|
||||
local OverworldBattle = V.require("OverworldBattle")
|
||||
local BattleExit = V.require("BattleExit")
|
||||
local Shiny = V.require("Shiny")
|
||||
local ShinyBattle = V.require("ShinyBattle")
|
||||
local ShinyUI = V.require("ShinyUI")
|
||||
local DayNight = V.require("DayNight")
|
||||
@@ -471,6 +472,21 @@ end
|
||||
-- its own: 3D WORLD is exactly the rows WITHOUT it, so that whole
|
||||
-- category empties out under FULL and takes itself off the menu.
|
||||
local SETTINGS = {
|
||||
-- ------- the top-level menu -- settings that are about the GAME
|
||||
--
|
||||
-- SettingsMenu.ROOT as a `cat` puts a row on the DRAMATIC SHAPE screen
|
||||
-- itself rather than inside one of the four categories, which is right
|
||||
-- here: the categories are the diorama, the fights, what the look costs
|
||||
-- and the headset, and how often a shiny appears is none of those.
|
||||
--
|
||||
-- `full` for the battle rows' reason: FULL is a preset for the LOOK, and
|
||||
-- an encounter rate is a rule of the game. A player inside FULL must be
|
||||
-- able to reach it, and FULL must never set it.
|
||||
{ Shiny.setting,
|
||||
"How often a wild Pokemon turns up shiny. 1:8192 is the games' own "
|
||||
.. "rate, and every rung below it is twice as often as the one above.",
|
||||
cat = SettingsMenu.ROOT, full = true },
|
||||
|
||||
-- ------- 3D WORLD -- the diorama's own knobs, every one of them FULL's
|
||||
{ VoxelGrid.setting, "One-pixel wireframe along every voxel edge.",
|
||||
cat = "world" },
|
||||
@@ -529,8 +545,8 @@ local SETTINGS = {
|
||||
-- `full` for the AA reason: additive shafts are fill rate, and under 4X
|
||||
-- supersampling that is a question about the hardware, not the look.
|
||||
{ ForestAtmos.setting,
|
||||
"Haze and light shafts in the deep woods, with pollen in the beams by "
|
||||
.. "day and fireflies over tall grass at night.",
|
||||
"Haze and volumetric light shafts in the deep woods, with pollen in the "
|
||||
.. "beams by day and fireflies at night.",
|
||||
cat = "perf", full = true },
|
||||
-- `full` on AA's reasoning below, and for the same reason: the sun's pass
|
||||
-- is the most expensive thing in the frame after the geometry, so this is
|
||||
|
||||
@@ -4686,84 +4686,6 @@ ForestAtmos.setting:sync("off")
|
||||
T.check(ForestAtmos.frame(fmap, DayNight.T.day) == nil,
|
||||
"OFF answers no frame at all: no fog uniform, no draw, no spend")
|
||||
ForestAtmos.setting:sync("full")
|
||||
|
||||
-- ------- and the fireflies get out of the forest
|
||||
--
|
||||
-- The same particle -- same mesh, same blinking shader, same hour's ramp --
|
||||
-- dealt over a map's TALL GRASS rather than over its whole volume, and on
|
||||
-- every outdoor map rather than the one with an entry. Grass is the entry.
|
||||
|
||||
-- the hour reads the same whether a map has an atmosphere or not, which is
|
||||
-- the point of pulling it out of frame(): the two cannot drift apart
|
||||
T.check(ForestAtmos.fireflyLevel(DayNight.T.night) > 0.5,
|
||||
"the night shift is out at midnight on a map with no entry either")
|
||||
T.check(ForestAtmos.fireflyLevel(DayNight.T.day) < 0.05,
|
||||
"and in by day")
|
||||
T.eq(ForestAtmos.fireflyLevel(DayNight.T.night),
|
||||
ForestAtmos.frame(fmap, DayNight.T.night).fireflyLevel,
|
||||
"the forest's own answer IS that answer -- one ramp, read twice")
|
||||
|
||||
-- the deal: over the cells handed in, one seed, one arrangement
|
||||
local meadow = {}
|
||||
for cx = 4, 9 do
|
||||
for cy = 4, 9 do meadow[#meadow + 1] = { cx, cy } end
|
||||
end
|
||||
local g1 = ForestAtmos.grassLayout(meadow, 4242)
|
||||
local g2 = ForestAtmos.grassLayout(meadow, 4242)
|
||||
T.eq(#g1, 29, "a 36-cell patch musters fireflies at the default density")
|
||||
local steady = true
|
||||
for i = 1, #g1 do
|
||||
steady = steady and g1[i].x == g2[i].x and g1[i].z == g2[i].z
|
||||
end
|
||||
T.check(steady, "and deals them the same way on every visit")
|
||||
T.check(#ForestAtmos.grassLayout(meadow, 99) > 0
|
||||
and ForestAtmos.grassLayout(meadow, 99)[1].x ~= g1[1].x,
|
||||
"a different map's seed deals a different swarm")
|
||||
|
||||
-- every one of them stands OVER the grass it was dealt for, in the tufts'
|
||||
-- own height band -- Structures stands a blade to y = 16
|
||||
local overGrass, lowDown = true, true
|
||||
for _, fly in ipairs(g1) do
|
||||
local cx, cy = math.floor(fly.x / 16), math.floor(fly.z / 16)
|
||||
if cx < 4 or cx > 9 or cy < 4 or cy > 9 then overGrass = false end
|
||||
if fly.y < 0 or fly.y > 16 then lowDown = false end
|
||||
end
|
||||
T.check(overGrass, "every firefly is dealt into a cell that HAS grass in it")
|
||||
T.check(lowDown, "and low over the blades rather than up in the air")
|
||||
|
||||
-- the knobs, and the cap that keeps a big route from dealing thousands
|
||||
T.eq(#ForestAtmos.grassLayout(meadow, 1, 2.0), 72,
|
||||
"the density knob musters more of them per cell")
|
||||
T.eq(#ForestAtmos.grassLayout(meadow, 1, 2.0, 10), 10,
|
||||
"and the cap is the ceiling over it")
|
||||
T.eq(#ForestAtmos.grassLayout({}, 1), 0,
|
||||
"a map with no grass on it deals nothing at all")
|
||||
|
||||
-- and the map-level gate: outdoors or under a canopy, never in a cave.
|
||||
-- A 4x4-block map is 8x8 cells; grass down the middle two columns.
|
||||
local function meadowMap(id, def)
|
||||
return {
|
||||
id = id, def = def, widthCells = 8, heightCells = 8,
|
||||
isGrassCell = function(_, cx, cy)
|
||||
return cx >= 3 and cx <= 4 and cy >= 1 and cy <= 6
|
||||
end,
|
||||
}
|
||||
end
|
||||
local route = meadowMap("ROUTE_1", { width = 4, height = 4,
|
||||
tileset = "OVERWORLD" })
|
||||
T.check(#ForestAtmos.grassFliesFor(route) > 0,
|
||||
"a route with tall grass on it has fireflies without an entry anywhere")
|
||||
local cave = meadowMap("MT_MOON_1F", { width = 4, height = 4,
|
||||
tileset = "CAVERN" })
|
||||
T.check(ForestAtmos.grassFliesFor(cave) == nil,
|
||||
"and a cave has none, whatever its collision tiles say -- no hour "
|
||||
.. "reaches it, so no night ever falls in it")
|
||||
local wood = meadowMap("VIRIDIAN_FOREST", { width = 4, height = 4,
|
||||
tileset = "FOREST" })
|
||||
T.check(#ForestAtmos.grassFliesFor(wood) > 0,
|
||||
"the canopy map is not outdoor and gets them anyway: night still FALLS "
|
||||
.. "in a forest, and it is the map they were drawn for")
|
||||
ForestAtmos.invalidate()
|
||||
end
|
||||
|
||||
-- ------- a shadow keeps hold of the feet that throw it
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
-- Driver: screenshot the fireflies over tall grass.
|
||||
--
|
||||
-- The forest's own particle, dealt over a map's grass instead of over its
|
||||
-- air, on every outdoor map rather than the one with an atmosphere entry
|
||||
-- (see ForestAtmos, "the grass fireflies"). What this has to show:
|
||||
--
|
||||
-- * a route with tall grass on it, at night, with lights over the grass
|
||||
-- and none over the road beside it -- the whole point of placing them
|
||||
-- by isGrassCell rather than by the grass GRAPHIC;
|
||||
-- * the same vantage by DAY, which must have none at all;
|
||||
-- * a MAP SEAM at night: the swarm has to carry onto the connected
|
||||
-- neighbour, whose grass is drawn in full;
|
||||
-- * Viridian Forest, where the grass swarm stands alongside the
|
||||
-- authored map-wide one and neither should read as double;
|
||||
-- * and the row OFF, which must be pixel-identical to a run from before
|
||||
-- the feature existed.
|
||||
--
|
||||
-- Deterministic on the same recipe forest_fog_shots runs: encounters
|
||||
-- killed, tile animation frozen, the atmosphere's clock pinned -- so an
|
||||
-- AB_TAG=before/after pair diffs clean. The grass deal itself is seeded
|
||||
-- off the map id and never moves.
|
||||
--
|
||||
-- SHOT_DIR=.scratchpad/grassfly AB_TAG=after \
|
||||
-- POKEPORT_DRIVER=mods/DramaticShapeVoxelMod/tests/grass_firefly_shots.lua \
|
||||
-- lovec.exe .
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local Pipelines = require("src.render.Pipelines")
|
||||
|
||||
local ROOT = (os.getenv("SHOT_DIR") or ".scratchpad/grassfly")
|
||||
.. "/" .. (os.getenv("AB_TAG") or "after")
|
||||
-- U.shot's own mkdir is Unix-flavoured and fails silently on Windows
|
||||
pcall(os.execute, 'mkdir -p "' .. ROOT .. '" 2>/dev/null')
|
||||
pcall(os.execute, 'mkdir "' .. ROOT:gsub("/", "\\") .. '" 2>nul')
|
||||
|
||||
local exports = game.mods and game.mods.exports
|
||||
local handle = exports and exports.DRAMATIC_SHAPE
|
||||
if not (handle and handle.lib) then
|
||||
U.log("DRAMATIC_SHAPE is not loaded -- check the ds_fp_ceiling conflict")
|
||||
return
|
||||
end
|
||||
local lib = handle.lib
|
||||
local DayNight = lib.require("DayNight")
|
||||
local ChunkMesher = lib.require("ChunkMesher")
|
||||
local Voxel = lib.require("VoxelState")
|
||||
local ForestAtmos = lib.require("ForestAtmos")
|
||||
|
||||
-- Prove the running game is reading THIS copy of the file before any
|
||||
-- shot is trusted: an install in the save dir shadows the repo, and a
|
||||
-- stale one makes an unchanged AFTER look like a broken feature.
|
||||
if not ForestAtmos.grassFliesFor then
|
||||
U.log("the loaded ForestAtmos has no grassFliesFor -- a STALE copy is "
|
||||
.. "being read, not this repo. Shots would be meaningless.")
|
||||
return
|
||||
end
|
||||
U.log("grass fireflies present in the loaded mod -- shooting")
|
||||
|
||||
-- ------- the determinism recipe (see forest_fog_shots)
|
||||
require("src.world.OverworldController").rollEncounter =
|
||||
function() return nil end
|
||||
local TileRenderer = require("src.render.TileRenderer")
|
||||
TileRenderer.tick = function() end
|
||||
TileRenderer.animFrame = function() return 0 end
|
||||
local Zoom = require("src.render.Zoom")
|
||||
pcall(function()
|
||||
game.save.options.zoom = 1
|
||||
Zoom.applyOptions(game.save.options)
|
||||
end)
|
||||
ForestAtmos.frozen = true
|
||||
ForestAtmos.time = 5
|
||||
|
||||
local function setTime(value)
|
||||
DayNight.setting:sync(value)
|
||||
DayNight.update(0)
|
||||
end
|
||||
|
||||
local function settle()
|
||||
for _ = 1, 900 do
|
||||
if ChunkMesher.pending() == 0 then break end
|
||||
U.wait(1)
|
||||
end
|
||||
for _ = 1, 300 do
|
||||
if Voxel.t >= 1 and Voxel.ready and ChunkMesher.pending() == 0 then
|
||||
break
|
||||
end
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(40)
|
||||
end
|
||||
|
||||
local function go(mapId, x, y, face, rung)
|
||||
U.teleport(game, mapId, x, y, face or "up")
|
||||
Pipelines.setLevel("voxel", rung or 5)
|
||||
Pipelines.setLevel("tiltshift", 0) -- judge the lights, not the blur
|
||||
settle()
|
||||
end
|
||||
|
||||
ForestAtmos.setting:sync("full")
|
||||
|
||||
-- ------- find a route the deal actually put fireflies on
|
||||
--
|
||||
-- Rather than guessing a cell with grass in it, ask the layout: it is
|
||||
-- the same list the mesh is built from, so a vantage picked out of it
|
||||
-- is guaranteed to have lights in frame.
|
||||
local ROUTES = { "ROUTE_1", "ROUTE_2", "ROUTE_22", "ROUTE_3" }
|
||||
local routeId, spotX, spotY
|
||||
for _, id in ipairs(ROUTES) do
|
||||
if game.data.maps and game.data.maps[id] then
|
||||
go(id, 5, 5)
|
||||
local map = game.stack:top() and game.stack:top().map
|
||||
local flies = map and ForestAtmos.grassFliesFor(map)
|
||||
if flies and #flies > 0 then
|
||||
-- the middle of the swarm, so the frame holds many rather than one
|
||||
local sx, sy, n = 0, 0, 0
|
||||
for _, fly in ipairs(flies) do
|
||||
sx, sy, n = sx + fly.x, sy + fly.z, n + 1
|
||||
end
|
||||
routeId = id
|
||||
spotX = math.floor(sx / n / 16)
|
||||
spotY = math.floor(sy / n / 16)
|
||||
U.log(("%s: %d fireflies over its grass, centre cell %d,%d")
|
||||
:format(id, #flies, spotX, spotY))
|
||||
break
|
||||
end
|
||||
U.log(id .. ": no grass fireflies dealt")
|
||||
end
|
||||
end
|
||||
|
||||
if not routeId then
|
||||
U.log("no route in this dataset got a grass swarm -- nothing to shoot")
|
||||
return
|
||||
end
|
||||
|
||||
-- ------- the route: night, then the same frame by day
|
||||
setTime("night")
|
||||
go(routeId, spotX, spotY + 3, "up")
|
||||
U.shot(game, ROOT .. "/10_route_night.png")
|
||||
go(routeId, spotX, spotY + 3, "up", 6) -- 1ST: eye level in the grass
|
||||
U.shot(game, ROOT .. "/11_route_night_fp.png")
|
||||
setTime("day")
|
||||
go(routeId, spotX, spotY + 3, "up")
|
||||
U.shot(game, ROOT .. "/12_route_day.png") -- must have none at all
|
||||
setTime("dusk")
|
||||
go(routeId, spotX, spotY + 3, "up")
|
||||
U.shot(game, ROOT .. "/13_route_dusk.png") -- coming on, not yet full
|
||||
|
||||
-- ------- the seam: the swarm has to carry onto the connected neighbour
|
||||
--
|
||||
-- Stand at the map's own edge looking out of it. The neighbour's ground,
|
||||
-- trees and grass are all drawn; if its fireflies were not, the picture
|
||||
-- would have a line across it where the lights stopped.
|
||||
setTime("night")
|
||||
do
|
||||
local map = game.stack:top() and game.stack:top().map
|
||||
local h = map and map.heightCells or 20
|
||||
go(routeId, spotX, math.max(h - 2, 0), "down")
|
||||
U.shot(game, ROOT .. "/20_seam_south_night.png")
|
||||
go(routeId, spotX, 1, "up")
|
||||
U.shot(game, ROOT .. "/21_seam_north_night.png")
|
||||
end
|
||||
|
||||
-- ------- Viridian Forest: the authored swarm and the grass one together
|
||||
if game.data.maps and game.data.maps.VIRIDIAN_FOREST then
|
||||
setTime("night")
|
||||
go("VIRIDIAN_FOREST", 17, 20)
|
||||
U.shot(game, ROOT .. "/30_forest_night.png")
|
||||
go("VIRIDIAN_FOREST", 16, 24)
|
||||
U.shot(game, ROOT .. "/31_forest_night_b.png")
|
||||
setTime("day")
|
||||
go("VIRIDIAN_FOREST", 17, 20)
|
||||
U.shot(game, ROOT .. "/32_forest_day.png") -- pollen, no fireflies
|
||||
end
|
||||
|
||||
-- ------- the controls
|
||||
setTime("night")
|
||||
ForestAtmos.setting:sync("low")
|
||||
go(routeId, spotX, spotY + 3, "up")
|
||||
U.shot(game, ROOT .. "/40_route_night_low.png") -- the tuft cards alone
|
||||
ForestAtmos.setting:sync("off")
|
||||
go(routeId, spotX, spotY + 3, "up")
|
||||
U.shot(game, ROOT .. "/41_route_night_off.png") -- a pre-feature run
|
||||
|
||||
ForestAtmos.setting:sync("full")
|
||||
ForestAtmos.frozen = false
|
||||
setTime("day")
|
||||
U.log("done -- " .. ROOT)
|
||||
end
|
||||
@@ -133,6 +133,33 @@ do
|
||||
eq(Shiny.ODDS_DENOM, saved, "and a sane one is accepted")
|
||||
end
|
||||
|
||||
do
|
||||
-- ------- the row on the menu
|
||||
--
|
||||
-- The ladder's first rung is both the default and the fallback, so the
|
||||
-- canonical 1:8192 has to be it: a player who never opens the menu, and a
|
||||
-- corrupted options.lua, must both land on the games' own rate.
|
||||
local s = Shiny.setting
|
||||
eq(s.values[1], 8192, "the ladder starts at the canonical rate")
|
||||
eq(s.labels[1], "1:8192", "and says so in the 1:# the row shows")
|
||||
eq(s.labels[#s.labels], "1:1", "the last rung is every encounter")
|
||||
eq(#s.values, #s.labels, "every rung has a label")
|
||||
for i = 2, #s.values do
|
||||
eq(s.values[i] * 2, s.values[i - 1],
|
||||
("rung %d is twice as often as the one above"):format(i))
|
||||
end
|
||||
|
||||
-- and the roll READS it. Pulled rather than pushed, so a value written by
|
||||
-- the mod manager's page -- which notifies nobody -- is seen too.
|
||||
local before = s:read()
|
||||
s:setValue(512)
|
||||
Shiny.unpinOdds()
|
||||
eq(Shiny.odds(), 512, "the roll follows the row without being told")
|
||||
eq(Shiny.ODDS_DENOM, 512, "and the live field is written through")
|
||||
s:setIndex(before)
|
||||
Shiny.setOdds(8192) -- and pinned again, for the blocks below
|
||||
end
|
||||
|
||||
do
|
||||
-- stats must follow the DVs, and a full-health mon must stay full: a wild
|
||||
-- mon that appears at less than full HP is visible in the first frame
|
||||
|
||||
Reference in New Issue
Block a user