Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c31dcca280 | ||
|
|
bfa3e1ce65 | ||
|
|
59f26dc5af | ||
|
|
bb6b34c1b6 | ||
|
|
302bb2b764 | ||
|
|
90722d1c04 | ||
|
|
22dca72f39 | ||
|
|
27f415d4af | ||
|
|
b8f2422433 | ||
|
|
62ebb27d25 | ||
|
|
979470f306 | ||
|
|
18f31b322a |
No files matched your search
+52
-16
@@ -36,8 +36,17 @@ rules in Lua. Countdowns may tick locally as display only.
|
||||
`screens/game/game.gui_script` only forwards the lifecycle to `screens/game/core/main.lua`
|
||||
(and on a hot reload re-requires it, see Fast iteration). The screen is rebuilt from the
|
||||
state: every new state (a response, a prediction) or a window/UI-scale change sets
|
||||
`g.dirty`, and the next update drops the Druid instance and every layout node and builds
|
||||
them again. `g` is the gui script's `self`.
|
||||
`g.dirty`, and the next update builds again what changed. The regions (banner, header,
|
||||
moon list, main panel, aside) and costly parts inside them (the market's ticker, chart and
|
||||
sparklines; Store, Equipment and Advisors cards through `card.grid`'s `key`/`sig`) are
|
||||
blocks (`modules/blocks.lua`): one whose signature (a serialization of everything it
|
||||
shows: its state keys, the view, busy buttons, ready counts on our clock) is unchanged
|
||||
keeps its nodes and Druid components. A full rebuild (drops the Druid instance and every
|
||||
layout node) happens on the first state, a size change, the narrow layout and
|
||||
`g.dirty_all` (theme, reduced motion, mute, scroll offsets set by code, hot reload). What
|
||||
a region shows from the state is listed in `core/main.lua` (`SECTION_KEYS`, `ASIDE_KEYS`,
|
||||
the header's and moon list's sigs): a section that reads a new state key must add it
|
||||
there, or it won't update. `g` is the gui script's `self`.
|
||||
|
||||
Core (`screens/game/core/`):
|
||||
- `main.lua`: init (layers, session, helpers, autoplay), update (rebuild when dirty,
|
||||
@@ -107,23 +116,31 @@ table, past moon, first-moon pitch; well helpers `supply_bar`/`supply_max`/`tool
|
||||
- optional `M.aside(g, root, w, h, H)` (section): fills the aside instead of the moon
|
||||
viewport, return true when it did; or `M.aside_below(g, root, pad, cy, inner, limit, H)`
|
||||
for content under the viewport.
|
||||
- optional `M.update(g, dt)`: every frame. optional `M.teardown(g)`: before every rebuild
|
||||
and when the screen goes away (cancel timers, drop references to nodes).
|
||||
- optional `M.update(g, dt)`: every frame. optional `M.teardown(g)`: before its region is
|
||||
built again and when the screen goes away (cancel timers, drop references to nodes).
|
||||
- What a module gets on `g`: `state`, `view`, `size` ({W, H, ui scale}), `druid` (made
|
||||
again every build), `fx`, `toasts`, `reveal`, `dialog`, `tween`, `fields` (text field
|
||||
again by a full rebuild), `fx`, `toasts`, `reveal`, `dialog`, `tween`, `fields` (text field
|
||||
state), `scroll_pos` (scroll offsets by key), `pointers` (mouse-move handlers, reset
|
||||
every build), `frames` (cards for the main panel's staggered entrance), `moon_nodes`,
|
||||
`well_nodes`, `tab_nodes` (for effects), `busy`, `inflight`.
|
||||
- What must survive a rebuild lives on `g` (offsets, fields, eased values) or on a layer
|
||||
outside `g.layout` (toasts, reveal, dialog, effects). Never keep a layout node across
|
||||
builds.
|
||||
builds outside its block.
|
||||
- Per-build registries (test ids and focus, tooltips, tickers, `g.inputs`,
|
||||
`g.scroll_areas`, `g.pointers`, fx anchors, autoplay) are reset by every build; a kept
|
||||
block makes its registrations again from a journal: a new registry must note them
|
||||
(`blocks.note(fn, ...)`, or `blocks.push`/`blocks.put` for a list or map on g). Code
|
||||
that builds outside a build (a list making rows as it scrolls, a field's on_change)
|
||||
runs through `blocks.bound(fn)` so what it makes belongs to its block. A kept block
|
||||
moved under another parent finds its buttons' clip stencil again.
|
||||
- A new section: a module in `screens/game/`, an entry in `M.SECTIONS`
|
||||
(`core/main.lua`), and a tab name in `header.lua` `SECTIONS` if it has one.
|
||||
|
||||
Entrances (enter.js): the main panel enters when the section or moon changes, the aside
|
||||
when the well or drill changes, both on the first state and after the reveal closes
|
||||
(`g.enter_all`); cards registered in `g.frames` (components/card) follow,
|
||||
staggered. Not on other rebuilds.
|
||||
staggered. Not on other rebuilds. While the reveal card flies into the moon list (Start
|
||||
mining) a new state waits for it to land.
|
||||
|
||||
## Components (`components/`)
|
||||
Build new UI from these instead of coordinate math. Each file's header documents its API.
|
||||
@@ -241,6 +258,19 @@ the markup colors). Fields refuse `<` and `&`. Measuring (`ui.text_width`,
|
||||
(the engine's GLFW window to its monitor and back; Defold has no runtime fullscreen).
|
||||
Changing it rebuilds the engine on the build server. On the web it builds as stubs; the
|
||||
browser's Fullscreen API is used instead (`modules/screen.lua`).
|
||||
- `mmterrain/`: native extension, the moon generator in C (`src/terrain.c`, a port of
|
||||
terrain.js's generation, same doubles in the same order; there is no Lua generator).
|
||||
User rule: C only (`.c`, Lua C API); C++ only where the dmSDK forces it,
|
||||
as a C-style shim (free functions and plain structs only: no classes, methods,
|
||||
templates, STL, references, own namespaces, exceptions or RTTI; `src/buffer_shim.cpp`:
|
||||
the buffer API). The extender compiles `.c`
|
||||
files as C++ ("treating 'c' input as 'c++'"), so the C must also be valid C++ and
|
||||
what links across files is `extern "C"`; the registration (`DM_DECLARE_EXTENSION` is
|
||||
C++) is written out in `mmterrain.c`. Lua: `mmterrain.new(terrain.native_params(p))`,
|
||||
`start` (a thread of its own where there are threads: desktop), `step(gen, secs)` (the
|
||||
web: no threads), `done`, `vertices`, `fill(gen, buf[, from, to])` (the triangle strip
|
||||
straight into the mesh buffer's streams), `result` (the flat arrays), `free` (header of
|
||||
`mmterrain.c`). `moon.script` makes every moon mesh with it.
|
||||
- `web/index.html`: the web build's page (`[html5] htmlfile`; see Web build).
|
||||
- `screens/game/`: the game screen (above). Also `market.lua` (`ticker.lua` (pauses on hover; items not in the Tab order: `test.register(..., no_focus)`), `sale.lua` (the sell power-down, kept
|
||||
by time on g and rebound every build; fx.lua asks `g.on_sale` for the coins' origin),
|
||||
@@ -288,9 +318,12 @@ file),
|
||||
other two lack: `₥`, `★`, `◆`, `▲`, `▼`, `●`, `›`, `≈`, Greek); distance field, built
|
||||
at 40px and scaled. OFL 1.1. `assets/ui.atlas`: white, frame, outline, sphere, glow,
|
||||
fade, ring, spark, speaker, sun and moon icons; tinted per node.
|
||||
- The moon viewport: `modules/moon/terrain.lua` ports the server's `terrain.js`
|
||||
(`scripts/moon-check.mjs` compares them), `modules/moon/moon.lua` is the API
|
||||
(`moon.show(id, node)`, `hide`, `mystery`, `reveal`, `skip_reveal`, `revealing`),
|
||||
- The moon viewport: `modules/moon/terrain.lua` ports the settings side of the server's
|
||||
`terrain.js` (defaults, resolve, traits, colors, the cube-sphere and its strip), and
|
||||
`mmterrain/` the generation, in C (`scripts/moon-check.mjs` compares them with
|
||||
terrain.js, many ids and settings; in `make check`), `modules/moon/moon.lua` is the API
|
||||
(`moon.show(id, node)`, `hide`, `mystery`, `reveal`, `skip_reveal`, `revealing`,
|
||||
`prefetch`: reveal.lua starts a bought moon's mesh as the buy-moon answer arrives),
|
||||
`moon/moon.collection` (`moon.script`: meshes, spin, drag, zoom, shroud, reveal),
|
||||
`moon/materials` (port of scene.js). `render/mm.render_script` draws the moon into an
|
||||
HDR target with bloom; the viewport node shows it through the `moon` gui material.
|
||||
@@ -317,9 +350,10 @@ fetches) to the server, so the game talks to its own origin; the server stays un
|
||||
server, which must allow CORS); the player id and settings are saved with sys.save
|
||||
(IndexedDB), X-MM-User sent as on desktop (the cookie works too, same origin).
|
||||
Fullscreen (F11, display dialog) through the browser's Fullscreen API, not saved or
|
||||
restored (the browser allows it only after a click or key). Moon generation is plain
|
||||
Lua there (no LuaJIT): grid capped at 96 (`WEB_RESOLUTION`) and 8 ms a frame (keeps
|
||||
60 fps; a checkpoint every few ms of work in terrain.lua), about 4 s per moon. Moon targets fall back to 8 bits without EXT_color_buffer_float.
|
||||
restored (the browser allows it only after a click or key). Moon generation is the
|
||||
mmterrain extension in wasm (no threads there): stepped with a share of each frame
|
||||
(30% of the frame time, 2–12 ms: ~2 ms at 144 Hz), the buffer filled over the next
|
||||
frames, grid capped at 128 (`WEB_RESOLUTION` in moon.script). Moon targets fall back to 8 bits without EXT_color_buffer_float.
|
||||
Variable frame rate there (`sys.set_update_frequency(0)` in main.script): the browser's
|
||||
requestAnimationFrame paces at the display's rate; `display.update_frequency = 60`
|
||||
(desktop's timer pacing) made the engine skip callbacks: 60 fps in a 2/3 cadence on
|
||||
@@ -337,7 +371,7 @@ fetches) to the server, so the game talks to its own origin; the server stays un
|
||||
|
||||
## Commands
|
||||
- `make build`: `bob build bundle` (debug) into `dist/x86_64-linux/MoonMiners/` (`build/` is
|
||||
bob's output; `build/x86_64-linux/dmengine` is the engine with our extension).
|
||||
bob's output; `build/x86_64-linux/dmengine` is the engine with our extensions).
|
||||
- `make run ARGS="--config=mm.api_base=http://localhost:3001"`: build and start.
|
||||
- `make web`, `make serve-web`: the browser build (Web build).
|
||||
- `make shot`: run under `xvfb-run` at 1280x800, save `dist/shot.png`, quit. `SHOT=`,
|
||||
@@ -461,8 +495,10 @@ Fails on: Lua syntax errors (every `.lua`, `.script`, `.gui_script`, `.render_sc
|
||||
luajit); stale debug flags (`[mm]` keys or values in `game.project` other than the shipped
|
||||
defaults, `DEBUG`/`XXX`/`HACK` markers in sources and shaders); characters the fonts lack
|
||||
(non-ASCII in Lua string literals: in the sym font or in the text fonts; server texts of
|
||||
a fixture player in every section: symbols in sym, the rest in the text fonts); and
|
||||
engine warnings in a `sections` scenario log (font batches, characters, text entries,
|
||||
a fixture player in every section: symbols in sym, the rest in the text fonts); the moon
|
||||
generator (`node scripts/moon-check.mjs [resolution] [seeds...]`:
|
||||
`mmterrain/src/terrain.c`, built for the host with `cc` and loaded into luajit through the
|
||||
FFI, and terrain.lua's settings side, against terrain.js; needs node, luajit, cc); and engine warnings in a `sections` scenario log (font batches, characters, text entries,
|
||||
nodes, animations, stencil, invalid markup tags, script errors). The scenario runs
|
||||
against `MM_API` (default http://localhost:3000) when it answers; otherwise the newest
|
||||
`dist/drive/*/game.log` is checked. `scripts/check.py --static` skips it.
|
||||
|
||||
@@ -31,6 +31,7 @@ local images = require("modules.images")
|
||||
local anim = require("modules.anim")
|
||||
local button = require("components.button")
|
||||
local panel = require("components.panel")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local S = theme.size
|
||||
|
||||
@@ -209,8 +210,11 @@ function M:set_height(h)
|
||||
end
|
||||
|
||||
-- A grid of cards from y; returns the y under it. make(c, item) fills one card.
|
||||
function M.grid(g, parent, x, y, inner, items, make)
|
||||
-- opts.key(item): keep the card (modules/blocks) while opts.sig(item) (what make shows
|
||||
-- besides the item; default the item alone) and its place stay the same.
|
||||
function M.grid(g, parent, x, y, inner, items, make, opts)
|
||||
local cols, cw = M.columns(inner)
|
||||
local keyed = opts and opts.key
|
||||
local i = 1
|
||||
while i <= #items do
|
||||
local row = {}
|
||||
@@ -219,18 +223,55 @@ function M.grid(g, parent, x, y, inner, items, make)
|
||||
if not item then
|
||||
break
|
||||
end
|
||||
local c = M.new(g, parent, x + (col - 1) * (cw + S.gap), y, cw)
|
||||
make(c, item)
|
||||
c:refresh()
|
||||
row[#row + 1] = c
|
||||
local r = { item = item, x = x + (col - 1) * (cw + S.gap) }
|
||||
if keyed then
|
||||
r.key = opts.key(item)
|
||||
r.sig = blocks.sig(r.x, y, cw, item, opts.sig and opts.sig(item) or false)
|
||||
r.old = blocks.peek(g, r.key, r.sig) -- (kept, if its row's height stays the same)
|
||||
end
|
||||
if r.old then
|
||||
r.natural = r.old.card.natural
|
||||
else
|
||||
if r.key then
|
||||
r.blk = blocks.open(g, r.key, r.sig, parent)
|
||||
end
|
||||
r.c = M.new(g, r.blk and r.blk.root or parent, r.x, y, cw)
|
||||
make(r.c, item)
|
||||
r.natural = r.c:refresh()
|
||||
if r.blk then
|
||||
r.blk.card = r.c
|
||||
blocks.close(g, r.blk)
|
||||
end
|
||||
end
|
||||
row[#row + 1] = r
|
||||
i = i + 1
|
||||
end
|
||||
local rh = 0
|
||||
for _, c in ipairs(row) do
|
||||
rh = math.max(rh, c.natural)
|
||||
for _, r in ipairs(row) do
|
||||
rh = math.max(rh, r.natural)
|
||||
end
|
||||
for _, c in ipairs(row) do
|
||||
c:set_height(rh)
|
||||
for _, r in ipairs(row) do
|
||||
if r.old and r.old.card.h == rh then
|
||||
r.blk = blocks.open(g, r.key, r.sig, parent) -- (kept)
|
||||
r.c = r.blk.card
|
||||
else
|
||||
if r.old then
|
||||
-- The row's height changed: made again at it.
|
||||
blocks.drop(g, r.old)
|
||||
r.blk = blocks.open(g, r.key, r.sig, parent)
|
||||
r.c = M.new(g, r.blk.root, r.x, y, cw)
|
||||
make(r.c, r.item)
|
||||
r.c:refresh()
|
||||
r.blk.card = r.c
|
||||
elseif r.blk then
|
||||
blocks.reopen(g, r.blk) -- (its hover and footer are the card's)
|
||||
end
|
||||
r.c:set_height(rh)
|
||||
if r.blk then
|
||||
blocks.close(g, r.blk)
|
||||
end
|
||||
end
|
||||
local c = r.c
|
||||
if g.frames then
|
||||
g.frames[#g.frames + 1] = panel.frame(c.root, c.edge, cw, rh) -- staggered entrance
|
||||
end
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
-- places it. A Druid component returned with it (a widget) is removed with the row.
|
||||
|
||||
local ui = require("modules.ui")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local M = {}
|
||||
M.__index = M
|
||||
@@ -24,7 +25,7 @@ local function vec(x, y)
|
||||
end
|
||||
|
||||
function M.new(g, parent, x, y, w, h, opts)
|
||||
local l = setmetatable({ g = g, parent = parent, x = x, y = y, w = w, h = h, key = opts.key, create = opts.create,
|
||||
local l = setmetatable({ g = g, parent = parent, x = x, y = y, w = w, h = h, key = opts.key, create = blocks.bound(opts.create), -- (rows made as it scrolls are its block's)
|
||||
focus_id = opts.focus_id, row_h = opts.row_h, items = {} }, M)
|
||||
g.scroll_pos = g.scroll_pos or {}
|
||||
l.view = ui.rect(parent, x, y, w, h, "text")
|
||||
@@ -49,8 +50,7 @@ function M.new(g, parent, x, y, w, h, opts)
|
||||
g.scroll_pos[l.key] = pos.y
|
||||
l:place_thumb(pos.y)
|
||||
end)
|
||||
g.scroll_areas = g.scroll_areas or {}
|
||||
g.scroll_areas[#g.scroll_areas + 1] = { view = l.view, content = l.content, key = l.key, max = 0, h = h, list = l, scroll = l.scroll }
|
||||
blocks.push(g, "scroll_areas", { view = l.view, content = l.content, key = l.key, max = 0, h = h, list = l, scroll = l.scroll })
|
||||
return l
|
||||
end
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
-- ... build into area.content (top-left based, like any parent) ...
|
||||
-- area:finish(content_height)
|
||||
--
|
||||
-- Screens rebuild their layout on every new state, so the offset is kept per key in
|
||||
-- A section built again makes its scroll area again, so the offset is kept per key in
|
||||
-- self.scroll_pos and restored on the next build. Buttons inside take the view as their
|
||||
-- click zone (Druid does that for nodes under a stencil), so hidden parts can't be
|
||||
-- clicked. Mouse wheel and drag scroll; a thin bar on the right shows where you are; its
|
||||
@@ -13,6 +13,7 @@
|
||||
-- g.scroll_areas (the test hook's scroll_to brings an element into view).
|
||||
|
||||
local ui = require("modules.ui")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -75,8 +76,7 @@ function Area:finish(content_h)
|
||||
gui.animate(thumb, "size.x", on and BAR_W + 2 or BAR_W, gui.EASING_OUTQUAD, 0.12)
|
||||
end)
|
||||
local s = self_.druid:new_scroll(self.view, self.content)
|
||||
self_.scroll_areas = self_.scroll_areas or {}
|
||||
self_.scroll_areas[#self_.scroll_areas + 1] = { view = self.view, content = self.content, key = self.key, max = max, h = h, scroll = s }
|
||||
blocks.push(self_, "scroll_areas", { view = self.view, content = self.content, key = self.key, max = max, h = h, scroll = s })
|
||||
|
||||
s:set_horizontal_scroll(false)
|
||||
s:set_extra_stretch_size(0)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
name: "MMTerrain"
|
||||
@@ -0,0 +1,37 @@
|
||||
// The two calls mmterrain.c needs from the engine's C++-only buffer API, as C functions:
|
||||
// the float32 x3 streams of the buffer at a Lua stack index, and marking it changed.
|
||||
#include <dmsdk/sdk.h>
|
||||
|
||||
static float* stream(dmBuffer::HBuffer buf, const char* name, uint32_t* count, uint32_t* stride)
|
||||
{
|
||||
void* data = 0;
|
||||
uint32_t components = 0;
|
||||
if (dmBuffer::GetStream(buf, dmHashString64(name), &data, count, &components, stride) != dmBuffer::RESULT_OK)
|
||||
return 0;
|
||||
if (components != 3)
|
||||
return 0;
|
||||
dmBuffer::ValueType type;
|
||||
if (dmBuffer::GetStreamType(buf, dmHashString64(name), &type, &components) != dmBuffer::RESULT_OK
|
||||
|| type != dmBuffer::VALUE_TYPE_FLOAT32)
|
||||
return 0;
|
||||
return (float*)data;
|
||||
}
|
||||
|
||||
extern "C" int mm_buffer_streams(lua_State* L, int index, float** pos, float** nor, float** col, unsigned* count, unsigned* stride)
|
||||
{
|
||||
dmBuffer::HBuffer buf = dmScript::CheckBufferUnpack(L, index);
|
||||
uint32_t c[3], s[3];
|
||||
*pos = stream(buf, "position", &c[0], &s[0]);
|
||||
*nor = stream(buf, "normal", &c[1], &s[1]);
|
||||
*col = stream(buf, "color", &c[2], &s[2]);
|
||||
if (!*pos || !*nor || !*col || s[0] != s[1] || s[0] != s[2])
|
||||
return 0;
|
||||
*count = c[0];
|
||||
*stride = s[0];
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" void mm_buffer_touch(lua_State* L, int index)
|
||||
{
|
||||
dmBuffer::UpdateContentVersion(dmScript::CheckBufferUnpack(L, index));
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/* mmterrain: the moon generator (terrain.c) for Lua. Plain C; the engine's buffer API is
|
||||
* C++ only, so buffer_shim.cpp reaches a buffer's streams for mmterrain.fill.
|
||||
*
|
||||
* local gen = mmterrain.new(terrain.native_params(p)) -- p: terrain.resolve's result
|
||||
* mmterrain.start(gen) -> true: it runs on a thread of its own (desktop);
|
||||
* false: step it (web)
|
||||
* mmterrain.step(gen, secs) -> true when done (works about secs; 0: to the end)
|
||||
* mmterrain.done(gen) -> true when done (threaded or stepped)
|
||||
* mmterrain.vertices(gen) -> the triangle strip's length (terrain.strip)
|
||||
* mmterrain.fill(gen, buf [, from, to]) fills buf's position, normal and color
|
||||
* streams (float32 x3, at least vertices(gen) long) with the
|
||||
* strip: its vertices from..to (1-based, inclusive; default
|
||||
* all)
|
||||
* mmterrain.result(gen) -> { n, count, positions, normals, colors, min_h, max_h }
|
||||
* mmterrain.free(gen) frees it now (else the garbage collector does)
|
||||
* Don't call anything but done/free on a started generator until done is true.
|
||||
*/
|
||||
#include <dmsdk/extension/extension.h>
|
||||
#include <dmsdk/dlib/align.h>
|
||||
|
||||
#include "terrain.h"
|
||||
|
||||
#define META "mmterrain.gen"
|
||||
|
||||
/* The extender compiles .c files in C++ mode (clang++, "treating 'c' input as 'c++'"), so
|
||||
* this file and terrain.c are valid as both; what links across files is extern "C". */
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* buffer_shim.cpp: a buffer's three float32 x3 streams, or 0. */
|
||||
int mm_buffer_streams(lua_State* L, int index, float** pos, float** nor, float** col, unsigned* count, unsigned* stride);
|
||||
void mm_buffer_touch(lua_State* L, int index);
|
||||
void MMTerrain(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
static mt_gen** check_slot(lua_State* L)
|
||||
{
|
||||
return (mt_gen**)luaL_checkudata(L, 1, META);
|
||||
}
|
||||
|
||||
static mt_gen* check_gen(lua_State* L)
|
||||
{
|
||||
mt_gen** slot = check_slot(L);
|
||||
if (!*slot)
|
||||
luaL_error(L, "mmterrain: generator already freed");
|
||||
return *slot;
|
||||
}
|
||||
|
||||
static mt_gen* check_done(lua_State* L)
|
||||
{
|
||||
mt_gen* g = check_gen(L);
|
||||
if (!mt_done(g))
|
||||
luaL_error(L, "mmterrain: generation not done");
|
||||
return g;
|
||||
}
|
||||
|
||||
static int New(lua_State* L)
|
||||
{
|
||||
double a[MT_PARAM_COUNT];
|
||||
mt_params p;
|
||||
mt_gen** slot;
|
||||
int i;
|
||||
luaL_checktype(L, 1, LUA_TTABLE);
|
||||
for (i = 0; i < MT_PARAM_COUNT; ++i) {
|
||||
lua_rawgeti(L, 1, i + 1);
|
||||
a[i] = luaL_checknumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
mt_params_from_array(a, MT_PARAM_COUNT, &p);
|
||||
slot = (mt_gen**)lua_newuserdata(L, sizeof(mt_gen*));
|
||||
*slot = 0;
|
||||
luaL_getmetatable(L, META);
|
||||
lua_setmetatable(L, -2);
|
||||
*slot = mt_new(&p);
|
||||
if (!*slot)
|
||||
return luaL_error(L, "mmterrain: out of memory");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int Start(lua_State* L)
|
||||
{
|
||||
lua_pushboolean(L, mt_start(check_gen(L)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int Step(lua_State* L)
|
||||
{
|
||||
mt_gen* g = check_gen(L);
|
||||
lua_pushboolean(L, mt_done(g) || mt_step(g, luaL_optnumber(L, 2, 0)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int Done(lua_State* L)
|
||||
{
|
||||
lua_pushboolean(L, mt_done(check_gen(L)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int Vertices(lua_State* L)
|
||||
{
|
||||
lua_pushinteger(L, mt_strip_len(mt_n(check_gen(L))));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int Fill(lua_State* L)
|
||||
{
|
||||
mt_gen* g = check_done(L);
|
||||
float *pos, *nor, *col;
|
||||
unsigned count, stride;
|
||||
if (!mm_buffer_streams(L, 2, &pos, &nor, &col, &count, &stride))
|
||||
return luaL_error(L, "mmterrain.fill: needs position, normal and color streams (float32 x3)");
|
||||
int len = mt_strip_len(mt_n(g));
|
||||
int from = (int)luaL_optinteger(L, 3, 1) - 1, to = (int)luaL_optinteger(L, 4, len);
|
||||
if (count < (unsigned)len)
|
||||
return luaL_error(L, "mmterrain.fill: buffer too short");
|
||||
mt_fill(g, pos, nor, col, (int)stride, from < 0 ? 0 : from, to > len ? len : to);
|
||||
mm_buffer_touch(L, 2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void push_array(lua_State* L, const double* a, int n)
|
||||
{
|
||||
int i;
|
||||
lua_createtable(L, n, 0);
|
||||
for (i = 0; i < n; ++i) {
|
||||
lua_pushnumber(L, a[i]);
|
||||
lua_rawseti(L, -2, i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
static int Result(lua_State* L)
|
||||
{
|
||||
mt_gen* g = check_done(L);
|
||||
int c = mt_count(g);
|
||||
lua_createtable(L, 0, 7);
|
||||
lua_pushinteger(L, mt_n(g));
|
||||
lua_setfield(L, -2, "n");
|
||||
lua_pushinteger(L, c);
|
||||
lua_setfield(L, -2, "count");
|
||||
push_array(L, mt_positions(g), c * 3);
|
||||
lua_setfield(L, -2, "positions");
|
||||
push_array(L, mt_normals(g), c * 3);
|
||||
lua_setfield(L, -2, "normals");
|
||||
push_array(L, mt_colors(g), c * 3);
|
||||
lua_setfield(L, -2, "colors");
|
||||
lua_pushnumber(L, mt_min_h(g));
|
||||
lua_setfield(L, -2, "min_h");
|
||||
lua_pushnumber(L, mt_max_h(g));
|
||||
lua_setfield(L, -2, "max_h");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int Free(lua_State* L)
|
||||
{
|
||||
mt_gen** slot = check_slot(L);
|
||||
mt_free(*slot); /* (joins a running thread) */
|
||||
*slot = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const luaL_Reg Module_methods[] = {
|
||||
{ "new", New },
|
||||
{ "start", Start },
|
||||
{ "step", Step },
|
||||
{ "done", Done },
|
||||
{ "vertices", Vertices },
|
||||
{ "fill", Fill },
|
||||
{ "result", Result },
|
||||
{ "free", Free },
|
||||
{ 0, 0 },
|
||||
};
|
||||
|
||||
static ExtensionResult Initialize(ExtensionParams* params)
|
||||
{
|
||||
lua_State* L = params->m_L;
|
||||
int top = lua_gettop(L);
|
||||
luaL_newmetatable(L, META);
|
||||
lua_pushcfunction(L, Free);
|
||||
lua_setfield(L, -2, "__gc");
|
||||
lua_pop(L, 1);
|
||||
luaL_register(L, "mmterrain", Module_methods);
|
||||
lua_pop(L, 1);
|
||||
(void)top;
|
||||
return EXTENSION_RESULT_OK;
|
||||
}
|
||||
|
||||
static ExtensionResult Finalize(ExtensionParams* params)
|
||||
{
|
||||
(void)params;
|
||||
return EXTENSION_RESULT_OK;
|
||||
}
|
||||
|
||||
/* DM_DECLARE_EXTENSION, written out for C (the macro's function is extern "C"). */
|
||||
static uint8_t DM_ALIGNED(16) g_Desc[128]; /* ExtensionDescBufferSize (a const, not a C constant) */
|
||||
|
||||
void MMTerrain(void)
|
||||
{
|
||||
ExtensionRegister((void*)g_Desc, sizeof(g_Desc), "MMTerrain", 0, 0, Initialize, Finalize, 0, 0);
|
||||
}
|
||||
@@ -0,0 +1,830 @@
|
||||
/* The moon generator (see terrain.h), a port of the server's web/static/js/terrain.js:
|
||||
* keep the two in sync (scripts/moon-check.mjs compares them). */
|
||||
#if !defined(_WIN32) && !defined(_POSIX_C_SOURCE)
|
||||
#define _POSIX_C_SOURCE 199309L /* clock_gettime */
|
||||
#endif
|
||||
|
||||
#include "terrain.h"
|
||||
|
||||
#include <math.h>
|
||||
#if MT_THREADS
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
/* The same rounding as the JS: no fused multiply-adds. */
|
||||
#if defined(__clang__)
|
||||
#pragma clang fp contract(off)
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC optimize("fp-contract=off")
|
||||
#endif
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
#define MAX_OCTAVES 10
|
||||
#define MAX_CRATERS 2000
|
||||
#define GRID 12
|
||||
|
||||
static double clock_secs(void)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
LARGE_INTEGER f, c;
|
||||
QueryPerformanceFrequency(&f);
|
||||
QueryPerformanceCounter(&c);
|
||||
return (double)c.QuadPart / (double)f.QuadPart;
|
||||
#else
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec + ts.tv_nsec * 1e-9;
|
||||
#endif
|
||||
}
|
||||
|
||||
static double dmin(double a, double b) { return a < b ? a : b; }
|
||||
static double dmax(double a, double b) { return a > b ? a : b; }
|
||||
static double lround_(double x) { return floor(x + 0.5); }
|
||||
|
||||
/* ------------------------------------------------------------ random */
|
||||
|
||||
/* mulberry32, uniform in [0, 1). */
|
||||
typedef struct { uint32_t a; } mt_rng;
|
||||
|
||||
static mt_rng rng_new(double seed)
|
||||
{
|
||||
mt_rng r;
|
||||
/* bit.tobit: the number modulo 2^32 */
|
||||
r.a = (uint32_t)(int64_t)fmod(seed, 4294967296.0);
|
||||
return r;
|
||||
}
|
||||
|
||||
static double rng_next(mt_rng* r)
|
||||
{
|
||||
uint32_t t;
|
||||
r->a += 0x6d2b79f5u;
|
||||
t = (r->a ^ (r->a >> 15)) * (1u | r->a);
|
||||
t = (t + (t ^ (t >> 7)) * (61u | t)) ^ t;
|
||||
return (double)(t ^ (t >> 14)) / 4294967296.0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ noise */
|
||||
|
||||
static const double GRAD3[36] = {
|
||||
1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, 0, 1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, -1, 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1,
|
||||
};
|
||||
static int PERM[512];
|
||||
static double GX[256], GY[256], GZ[256];
|
||||
static int perm_ready = 0;
|
||||
|
||||
static void perm_init(void)
|
||||
{
|
||||
int p[256], i;
|
||||
mt_rng r;
|
||||
if (perm_ready)
|
||||
return;
|
||||
for (i = 0; i < 256; ++i)
|
||||
p[i] = i;
|
||||
r = rng_new(1337);
|
||||
for (i = 255; i >= 1; --i) {
|
||||
int j = (int)floor(rng_next(&r) * (i + 1));
|
||||
int t = p[i];
|
||||
p[i] = p[j];
|
||||
p[j] = t;
|
||||
}
|
||||
for (i = 0; i < 512; ++i)
|
||||
PERM[i] = p[i & 255];
|
||||
for (i = 0; i < 256; ++i) {
|
||||
int g = (i % 12) * 3;
|
||||
GX[i] = GRAD3[g];
|
||||
GY[i] = GRAD3[g + 1];
|
||||
GZ[i] = GRAD3[g + 2];
|
||||
}
|
||||
perm_ready = 1;
|
||||
}
|
||||
|
||||
#define F3 (1.0 / 3.0)
|
||||
#define G3 (1.0 / 6.0)
|
||||
|
||||
/* floor without a libm call (x86_64's baseline has no rounding instruction); the same
|
||||
* value for |x| < 2^53. */
|
||||
static double ffloor(double x)
|
||||
{
|
||||
double t = (double)(int64_t)x;
|
||||
return t > x ? t - 1 : t;
|
||||
}
|
||||
|
||||
/* terrain.lua simplex3 (branch-free, the same values as the reference's if/else chains,
|
||||
* and faster here too: the corner order is unpredictable). */
|
||||
static double simplex3(double x, double y, double z)
|
||||
{
|
||||
const int* P = PERM;
|
||||
double s = (x + y + z) * F3;
|
||||
double i = ffloor(x + s), j = ffloor(y + s), k = ffloor(z + s);
|
||||
double t = (i + j + k) * G3;
|
||||
double x0 = x - (i - t), y0 = y - (j - t), z0 = z - (k - t);
|
||||
double a = x0 >= y0 ? 1 : 0; /* 1 + floor(x0 - y0) in the Lua: |x0 - y0| < 1 */
|
||||
double b = y0 >= z0 ? 1 : 0;
|
||||
double c = x0 >= z0 ? 1 : 0;
|
||||
double i1 = a * (b + (1 - b) * c);
|
||||
double j1 = (1 - a) * b;
|
||||
double k1 = 1 - i1 - j1;
|
||||
double i2 = a + (1 - a) * b * c;
|
||||
double j2 = a * b + 1 - a;
|
||||
double k2 = 2 - i2 - j2;
|
||||
int ii = (int)((int64_t)i & 255), jj = (int)((int64_t)j & 255), kk = (int)((int64_t)k & 255);
|
||||
int ii1 = (int)i1, jj1 = (int)j1, kk1 = (int)k1, ii2 = (int)i2, jj2 = (int)j2, kk2 = (int)k2;
|
||||
double tt, n, x1, y1, z1, x2, y2, z2, x3, y3, z3;
|
||||
int g;
|
||||
tt = dmax(0, 0.6 - x0 * x0 - y0 * y0 - z0 * z0);
|
||||
g = P[ii + P[jj + P[kk]]];
|
||||
tt = tt * tt;
|
||||
n = tt * tt * (GX[g] * x0 + GY[g] * y0 + GZ[g] * z0);
|
||||
x1 = x0 - i1 + G3; y1 = y0 - j1 + G3; z1 = z0 - k1 + G3;
|
||||
tt = dmax(0, 0.6 - x1 * x1 - y1 * y1 - z1 * z1);
|
||||
g = P[ii + ii1 + P[jj + jj1 + P[kk + kk1]]];
|
||||
tt = tt * tt;
|
||||
n = n + tt * tt * (GX[g] * x1 + GY[g] * y1 + GZ[g] * z1);
|
||||
x2 = x0 - i2 + 2 * G3; y2 = y0 - j2 + 2 * G3; z2 = z0 - k2 + 2 * G3;
|
||||
tt = dmax(0, 0.6 - x2 * x2 - y2 * y2 - z2 * z2);
|
||||
g = P[ii + ii2 + P[jj + jj2 + P[kk + kk2]]];
|
||||
tt = tt * tt;
|
||||
n = n + tt * tt * (GX[g] * x2 + GY[g] * y2 + GZ[g] * z2);
|
||||
x3 = x0 - 1 + 3 * G3; y3 = y0 - 1 + 3 * G3; z3 = z0 - 1 + 3 * G3;
|
||||
tt = dmax(0, 0.6 - x3 * x3 - y3 * y3 - z3 * z3);
|
||||
g = P[ii + 1 + P[jj + 1 + P[kk + 1]]];
|
||||
tt = tt * tt;
|
||||
n = n + tt * tt * (GX[g] * x3 + GY[g] * y3 + GZ[g] * z3);
|
||||
return 32 * n;
|
||||
}
|
||||
|
||||
static double fbm(double x, double y, double z, double octaves, double frequency, double persistence, double lacunarity)
|
||||
{
|
||||
double sum = 0, amp = 1, norm = 0, f = frequency, o, top = dmin(octaves, MAX_OCTAVES);
|
||||
for (o = 1; o <= top; o += 1) {
|
||||
sum = sum + simplex3(x * f, y * f, z * f) * amp;
|
||||
norm = norm + amp;
|
||||
amp = amp * persistence;
|
||||
f = f * lacunarity;
|
||||
}
|
||||
return sum / norm;
|
||||
}
|
||||
|
||||
static double billow(double x, double y, double z, double octaves, double frequency, double persistence, double lacunarity)
|
||||
{
|
||||
double sum = 0, amp = 1, norm = 0, f = frequency, o, top = dmin(octaves, MAX_OCTAVES);
|
||||
for (o = 1; o <= top; o += 1) {
|
||||
sum = sum + fabs(simplex3(x * f, y * f, z * f)) * amp;
|
||||
norm = norm + amp;
|
||||
amp = amp * persistence;
|
||||
f = f * lacunarity;
|
||||
}
|
||||
return sum / norm;
|
||||
}
|
||||
|
||||
static double ridged(double x, double y, double z, double octaves, double frequency, double persistence, double lacunarity,
|
||||
double sharpness, double gain)
|
||||
{
|
||||
double sum = 0, amp = 1, norm = 0, weight = 1, f = frequency, o, top = dmin(octaves, MAX_OCTAVES);
|
||||
for (o = 1; o <= top; o += 1) {
|
||||
double v = 1 - fabs(simplex3(x * f, y * f, z * f));
|
||||
v = pow(v, sharpness) * weight;
|
||||
weight = dmin(1, dmax(0, v * gain));
|
||||
sum = sum + v * amp;
|
||||
norm = norm + amp;
|
||||
amp = amp * persistence;
|
||||
f = f * lacunarity;
|
||||
}
|
||||
return sum / norm;
|
||||
}
|
||||
|
||||
static double smooth_min(double a, double b, double k)
|
||||
{
|
||||
double h;
|
||||
if (k <= 0)
|
||||
return dmin(a, b);
|
||||
h = dmin(1, dmax(0, (b - a + k) / (2 * k)));
|
||||
return a * h + b * (1 - h) - k * h * (1 - h);
|
||||
}
|
||||
|
||||
static double smooth_max(double a, double b, double k)
|
||||
{
|
||||
return -smooth_min(-a, -b, k);
|
||||
}
|
||||
|
||||
static double smoothstep(double e0, double e1, double x)
|
||||
{
|
||||
double d = e1 - e0, t;
|
||||
if (d == 0)
|
||||
d = 1e-6;
|
||||
t = dmin(1, dmax(0, (x - e0) / d));
|
||||
return t * t * (3 - 2 * t);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ colors */
|
||||
|
||||
int mt_params_from_array(const double* a, int n, mt_params* p)
|
||||
{
|
||||
int i = 0, k;
|
||||
if (n < MT_PARAM_COUNT)
|
||||
return 0;
|
||||
p->resolution = a[i++];
|
||||
p->technique = (int)a[i++];
|
||||
p->shape_strength = a[i++]; p->shape_frequency = a[i++]; p->shape_octaves = a[i++];
|
||||
p->shape_persistence = a[i++]; p->shape_lacunarity = a[i++]; p->shape_min = a[i++];
|
||||
p->ridge_strength = a[i++]; p->ridge_frequency = a[i++]; p->ridge_octaves = a[i++];
|
||||
p->ridge_persistence = a[i++]; p->ridge_lacunarity = a[i++]; p->ridge_sharpness = a[i++];
|
||||
p->ridge_gain = a[i++]; p->ridge_min = a[i++];
|
||||
p->warp_strength = a[i++]; p->warp_frequency = a[i++]; p->warp_octaves = a[i++];
|
||||
p->crater_count = a[i++]; p->crater_min_radius = a[i++]; p->crater_max_radius = a[i++];
|
||||
p->crater_size_bias = a[i++]; p->crater_depth = a[i++]; p->crater_rim_width = a[i++];
|
||||
p->crater_rim_steepness = a[i++]; p->crater_floor = a[i++]; p->crater_smoothness = a[i++];
|
||||
p->crater_ejecta = a[i++];
|
||||
for (k = 0; k < 3; ++k) p->low[k] = a[i++];
|
||||
for (k = 0; k < 3; ++k) p->mid[k] = a[i++];
|
||||
for (k = 0; k < 3; ++k) p->high[k] = a[i++];
|
||||
for (k = 0; k < 3; ++k) p->steep[k] = a[i++];
|
||||
for (k = 0; k < 3; ++k) p->patch[k] = a[i++];
|
||||
p->steep_threshold = a[i++]; p->steep_blend = a[i++]; p->patch_amount = a[i++];
|
||||
p->patch_frequency = a[i++]; p->grain = a[i++];
|
||||
for (k = 0; k < 3; ++k) p->offset[k] = a[i++];
|
||||
p->crater_seed = a[i++];
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ generator */
|
||||
|
||||
typedef struct crater {
|
||||
double x, y, z, r, reach;
|
||||
} crater;
|
||||
|
||||
enum { ST_DIRS, ST_CRATERS, ST_HEIGHTS, ST_MEAN, ST_POSITIONS, ST_NORMALS, ST_SEAMS, ST_NORMALIZE, ST_COLORS, ST_DONE };
|
||||
|
||||
struct mt_gen {
|
||||
mt_params p;
|
||||
#if MT_THREADS
|
||||
pthread_t thread;
|
||||
int threaded;
|
||||
int finished; /* (set by the thread, read with atomics) */
|
||||
#endif
|
||||
int n, row, per, count;
|
||||
int stage, i;
|
||||
double* dirs;
|
||||
int* keys;
|
||||
double *heights, *ejecta, *positions, *normals, *colors;
|
||||
crater* craters;
|
||||
int n_craters;
|
||||
int* cell_start; /* GRID^3 + 1 offsets into cell_list */
|
||||
int* cell_list;
|
||||
double x_max, mean, min_h, max_h;
|
||||
};
|
||||
|
||||
static const double FACES[6][9] = {
|
||||
{ 1, 0, 0, 0, 1, 0, 0, 0, 1 },
|
||||
{ -1, 0, 0, 0, 0, 1, 0, 1, 0 },
|
||||
{ 0, 1, 0, 0, 0, 1, 1, 0, 0 },
|
||||
{ 0, -1, 0, 1, 0, 0, 0, 0, 1 },
|
||||
{ 0, 0, 1, 1, 0, 0, 0, 1, 0 },
|
||||
{ 0, 0, -1, 0, 1, 0, 1, 0, 0 },
|
||||
};
|
||||
|
||||
mt_gen* mt_new(const mt_params* p)
|
||||
{
|
||||
mt_gen* g = (mt_gen*)calloc(1, sizeof(mt_gen));
|
||||
size_t c3;
|
||||
if (!g)
|
||||
return 0;
|
||||
perm_init();
|
||||
g->p = *p;
|
||||
g->n = (int)dmax(8, dmin(320, lround_(p->resolution)));
|
||||
g->row = g->n + 1;
|
||||
g->per = g->row * g->row;
|
||||
g->count = 6 * g->per;
|
||||
c3 = (size_t)g->count * 3;
|
||||
g->dirs = (double*)malloc(c3 * sizeof(double));
|
||||
g->keys = (int*)malloc((size_t)g->count * sizeof(int));
|
||||
g->heights = (double*)malloc((size_t)g->count * sizeof(double));
|
||||
g->ejecta = (double*)malloc((size_t)g->count * sizeof(double));
|
||||
g->positions = (double*)malloc(c3 * sizeof(double));
|
||||
g->normals = (double*)calloc(c3, sizeof(double));
|
||||
g->colors = (double*)malloc(c3 * sizeof(double));
|
||||
if (!g->dirs || !g->keys || !g->heights || !g->ejecta || !g->positions || !g->normals || !g->colors) {
|
||||
mt_free(g);
|
||||
return 0;
|
||||
}
|
||||
g->stage = ST_DIRS;
|
||||
return g;
|
||||
}
|
||||
|
||||
void mt_free(mt_gen* g)
|
||||
{
|
||||
if (!g)
|
||||
return;
|
||||
#if MT_THREADS
|
||||
if (g->threaded)
|
||||
pthread_join(g->thread, 0);
|
||||
#endif
|
||||
free(g->dirs);
|
||||
free(g->keys);
|
||||
free(g->heights);
|
||||
free(g->ejecta);
|
||||
free(g->positions);
|
||||
free(g->normals);
|
||||
free(g->colors);
|
||||
free(g->craters);
|
||||
free(g->cell_start);
|
||||
free(g->cell_list);
|
||||
free(g);
|
||||
}
|
||||
|
||||
static void dir_at(mt_gen* g, int v)
|
||||
{
|
||||
int n = g->n, row = g->row, f = v / g->per, r = v % g->per, j = r / row, i = r % row;
|
||||
const double* F = FACES[f];
|
||||
double s = ((double)i / n) * 2 - 1;
|
||||
double t = ((double)j / n) * 2 - 1;
|
||||
double cx = F[0] + s * F[3] + t * F[6];
|
||||
double cy = F[1] + s * F[4] + t * F[7];
|
||||
double cz = F[2] + s * F[5] + t * F[8];
|
||||
double x2 = cx * cx, y2 = cy * cy, z2 = cz * cz;
|
||||
g->dirs[v * 3] = cx * sqrt(1 - y2 / 2 - z2 / 2 + (y2 * z2) / 3);
|
||||
g->dirs[v * 3 + 1] = cy * sqrt(1 - x2 / 2 - z2 / 2 + (x2 * z2) / 3);
|
||||
g->dirs[v * 3 + 2] = cz * sqrt(1 - x2 / 2 - y2 / 2 + (x2 * y2) / 3);
|
||||
if (i == 0 || j == 0 || i == n || j == n) {
|
||||
int kx = (int)lround_(((cx + 1) / 2) * n), ky = (int)lround_(((cy + 1) / 2) * n), kz = (int)lround_(((cz + 1) / 2) * n);
|
||||
g->keys[v] = (kx * row + ky) * row + kz;
|
||||
} else {
|
||||
g->keys[v] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
static int cell_at(double v)
|
||||
{
|
||||
double cell = 2.0 / GRID;
|
||||
return (int)dmin(GRID - 1, dmax(0, floor((v + 1) / cell)));
|
||||
}
|
||||
|
||||
/* terrain.lua make_craters: the list, then each grid cell's craters in list order. */
|
||||
static int make_craters(mt_gen* g)
|
||||
{
|
||||
const mt_params* p = &g->p;
|
||||
int field = p->technique == MT_CRATER_FIELD;
|
||||
double min_r = field ? p->crater_min_radius * 0.8 : p->crater_min_radius;
|
||||
double max_r = field ? p->crater_max_radius * 0.6 : p->crater_max_radius;
|
||||
double scale = field ? 3 : 1;
|
||||
int count = (int)lround_(dmin(p->crater_count, MAX_CRATERS) * scale), i, c, x, y, z, total;
|
||||
mt_rng r = rng_new(p->crater_seed);
|
||||
int cells = GRID * GRID * GRID;
|
||||
int* fill;
|
||||
if (count < 0)
|
||||
count = 0;
|
||||
g->x_max = dmax(1 + p->crater_rim_width, sqrt(1 + dmax(0, p->crater_smoothness)));
|
||||
g->craters = (crater*)malloc((size_t)(count > 0 ? count : 1) * sizeof(crater));
|
||||
g->cell_start = (int*)calloc((size_t)cells + 1, sizeof(int));
|
||||
if (!g->craters || !g->cell_start)
|
||||
return 0;
|
||||
for (i = 0; i < count; ++i) {
|
||||
double zz = rng_next(&r) * 2 - 1;
|
||||
double a = rng_next(&r) * M_PI * 2;
|
||||
double q = sqrt(1 - zz * zz);
|
||||
double radius = min_r + (max_r - min_r) * pow(rng_next(&r), p->crater_size_bias);
|
||||
crater* k = &g->craters[i];
|
||||
k->x = q * cos(a);
|
||||
k->y = q * sin(a);
|
||||
k->z = zz;
|
||||
k->r = radius;
|
||||
k->reach = radius * g->x_max;
|
||||
}
|
||||
g->n_craters = count;
|
||||
/* Two passes: counts per cell, then the lists. */
|
||||
for (c = 0; c < count; ++c) {
|
||||
crater* k = &g->craters[c];
|
||||
for (x = cell_at(k->x - k->reach); x <= cell_at(k->x + k->reach); ++x)
|
||||
for (y = cell_at(k->y - k->reach); y <= cell_at(k->y + k->reach); ++y)
|
||||
for (z = cell_at(k->z - k->reach); z <= cell_at(k->z + k->reach); ++z)
|
||||
g->cell_start[(x * GRID + y) * GRID + z + 1]++;
|
||||
}
|
||||
for (i = 0; i < cells; ++i)
|
||||
g->cell_start[i + 1] += g->cell_start[i];
|
||||
total = g->cell_start[cells];
|
||||
g->cell_list = (int*)malloc((size_t)(total > 0 ? total : 1) * sizeof(int));
|
||||
fill = (int*)calloc((size_t)cells, sizeof(int));
|
||||
if (!g->cell_list || !fill) {
|
||||
free(fill);
|
||||
return 0;
|
||||
}
|
||||
for (c = 0; c < count; ++c) {
|
||||
crater* k = &g->craters[c];
|
||||
for (x = cell_at(k->x - k->reach); x <= cell_at(k->x + k->reach); ++x)
|
||||
for (y = cell_at(k->y - k->reach); y <= cell_at(k->y + k->reach); ++y)
|
||||
for (z = cell_at(k->z - k->reach); z <= cell_at(k->z + k->reach); ++z) {
|
||||
int cell = (x * GRID + y) * GRID + z;
|
||||
g->cell_list[g->cell_start[cell] + fill[cell]++] = c;
|
||||
}
|
||||
}
|
||||
free(fill);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void height_at(mt_gen* g, int i)
|
||||
{
|
||||
const mt_params* p = &g->p;
|
||||
int field = p->technique == MT_CRATER_FIELD;
|
||||
double x = g->dirs[i * 3], y = g->dirs[i * 3 + 1], z = g->dirs[i * 3 + 2];
|
||||
double px = x + p->offset[0], py = y + p->offset[1], pz = z + p->offset[2];
|
||||
double value, h, crater_h = 0, glow = 0;
|
||||
double shape_strength = field ? p->shape_strength * 0.4 : p->shape_strength;
|
||||
double smooth = dmax(0, p->crater_smoothness);
|
||||
double ejecta_reach = 1 + p->crater_rim_width * 2.5;
|
||||
double x_max = g->x_max;
|
||||
int cell, c, end;
|
||||
switch (p->technique) {
|
||||
case MT_RIDGED:
|
||||
value = ridged(px, py, pz, p->shape_octaves, p->shape_frequency, p->shape_persistence, p->shape_lacunarity,
|
||||
p->ridge_sharpness, p->ridge_gain);
|
||||
break;
|
||||
case MT_BILLOW:
|
||||
value = billow(px, py, pz, p->shape_octaves, p->shape_frequency, p->shape_persistence, p->shape_lacunarity);
|
||||
break;
|
||||
case MT_WARPED: {
|
||||
double wf = p->warp_frequency;
|
||||
double wx = fbm(px + 5.2, py + 1.3, pz, p->warp_octaves, wf, 0.5, 2);
|
||||
double wy = fbm(px, py + 9.7, pz + 2.8, p->warp_octaves, wf, 0.5, 2);
|
||||
double wz = fbm(px + 3.1, py, pz + 7.4, p->warp_octaves, wf, 0.5, 2);
|
||||
double s = p->warp_strength;
|
||||
value = (fbm(px + wx * s, py + wy * s, pz + wz * s, p->shape_octaves, p->shape_frequency, p->shape_persistence,
|
||||
p->shape_lacunarity) + 1) / 2;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
value = (fbm(px, py, pz, p->shape_octaves, p->shape_frequency, p->shape_persistence, p->shape_lacunarity) + 1) / 2;
|
||||
break;
|
||||
}
|
||||
h = dmax(0, value - p->shape_min) * shape_strength;
|
||||
if (p->ridge_strength != 0) {
|
||||
double r = ridged(px - 11.3, py + 4.1, pz - 7.9, p->ridge_octaves, p->ridge_frequency, p->ridge_persistence,
|
||||
p->ridge_lacunarity, p->ridge_sharpness, p->ridge_gain);
|
||||
h = h + dmax(0, r - p->ridge_min) * p->ridge_strength;
|
||||
}
|
||||
cell = (cell_at(x) * GRID + cell_at(y)) * GRID + cell_at(z);
|
||||
end = g->cell_start[cell + 1];
|
||||
for (c = g->cell_start[cell]; c < end; ++c) {
|
||||
const crater* k = &g->craters[g->cell_list[c]];
|
||||
double dx = x - k->x, dy = y - k->y, dz = z - k->z;
|
||||
double d2 = dx * dx + dy * dy + dz * dz;
|
||||
double reach = dmax(k->reach, k->r * ejecta_reach);
|
||||
if (d2 <= reach * reach) {
|
||||
double dist = sqrt(d2) / k->r;
|
||||
if (dist < x_max) {
|
||||
double cavity = dist * dist - 1;
|
||||
double rim_x = dmin(dist - 1 - p->crater_rim_width, 0);
|
||||
double rim = p->crater_rim_steepness * rim_x * rim_x;
|
||||
double shape = smooth_max(cavity, p->crater_floor, smooth);
|
||||
shape = smooth_min(shape, rim, smooth);
|
||||
crater_h = crater_h + shape * k->r;
|
||||
}
|
||||
if (dist > 0.8 && dist < ejecta_reach)
|
||||
glow = glow + 1 - smoothstep(1, ejecta_reach, dist);
|
||||
}
|
||||
}
|
||||
g->heights[i] = h + crater_h * p->crater_depth;
|
||||
g->ejecta[i] = dmin(1, glow);
|
||||
}
|
||||
|
||||
static void tri(mt_gen* g, int a, int b, int c)
|
||||
{
|
||||
const double* P = g->positions;
|
||||
double* N = g->normals;
|
||||
double e1x, e1y, e1z, e2x, e2y, e2z, nx, ny, nz;
|
||||
a *= 3;
|
||||
b *= 3;
|
||||
c *= 3;
|
||||
e1x = P[b] - P[a]; e1y = P[b + 1] - P[a + 1]; e1z = P[b + 2] - P[a + 2];
|
||||
e2x = P[c] - P[a]; e2y = P[c + 1] - P[a + 1]; e2z = P[c + 2] - P[a + 2];
|
||||
nx = e1y * e2z - e1z * e2y;
|
||||
ny = e1z * e2x - e1x * e2z;
|
||||
nz = e1x * e2y - e1y * e2x;
|
||||
N[a] += nx; N[a + 1] += ny; N[a + 2] += nz;
|
||||
N[b] += nx; N[b + 1] += ny; N[b + 2] += nz;
|
||||
N[c] += nx; N[c + 1] += ny; N[c + 2] += nz;
|
||||
}
|
||||
|
||||
typedef struct seam {
|
||||
int key, index;
|
||||
} seam;
|
||||
|
||||
static int seam_cmp(const void* a, const void* b)
|
||||
{
|
||||
const seam* x = (const seam*)a;
|
||||
const seam* y = (const seam*)b;
|
||||
if (x->key != y->key)
|
||||
return x->key < y->key ? -1 : 1;
|
||||
return x->index < y->index ? -1 : (x->index > y->index);
|
||||
}
|
||||
|
||||
/* Faces duplicate their edge vertices: each copy gets the sum (in index order, like the
|
||||
* Lua's ipairs over its group). */
|
||||
static int seams(mt_gen* g)
|
||||
{
|
||||
int count = g->count, i, m = 0, a, b, k;
|
||||
seam* list = (seam*)malloc((size_t)count * sizeof(seam));
|
||||
if (!list)
|
||||
return 0;
|
||||
for (i = 0; i < count; ++i)
|
||||
if (g->keys[i] >= 0) {
|
||||
list[m].key = g->keys[i];
|
||||
list[m].index = i;
|
||||
++m;
|
||||
}
|
||||
qsort(list, (size_t)m, sizeof(seam), seam_cmp);
|
||||
for (a = 0; a < m; a = b) {
|
||||
double sx = 0, sy = 0, sz = 0;
|
||||
for (b = a; b < m && list[b].key == list[a].key; ++b) {
|
||||
}
|
||||
if (b - a < 2)
|
||||
continue;
|
||||
for (k = a; k < b; ++k) {
|
||||
int o = list[k].index * 3;
|
||||
sx = sx + g->normals[o];
|
||||
sy = sy + g->normals[o + 1];
|
||||
sz = sz + g->normals[o + 2];
|
||||
}
|
||||
for (k = a; k < b; ++k) {
|
||||
int o = list[k].index * 3;
|
||||
g->normals[o] = sx;
|
||||
g->normals[o + 1] = sy;
|
||||
g->normals[o + 2] = sz;
|
||||
}
|
||||
}
|
||||
free(list);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void color_at(mt_gen* g, int i)
|
||||
{
|
||||
const mt_params* p = &g->p;
|
||||
int o = i * 3;
|
||||
double x = g->dirs[o], y = g->dirs[o + 1], z = g->dirs[o + 2];
|
||||
double ox = p->offset[0], oy = p->offset[1], oz = p->offset[2];
|
||||
double range = g->max_h - g->min_h;
|
||||
double e, te, c1, c2, c3, gg, steepness, ws, gr;
|
||||
const double *lo, *hi;
|
||||
if (range == 0)
|
||||
range = 1;
|
||||
e = (g->heights[i] - g->min_h) / range;
|
||||
if (e < 0.5) {
|
||||
lo = p->low; hi = p->mid; te = e * 2;
|
||||
} else {
|
||||
lo = p->mid; hi = p->high; te = (e - 0.5) * 2;
|
||||
}
|
||||
c1 = lo[0] + (hi[0] - lo[0]) * te;
|
||||
c2 = lo[1] + (hi[1] - lo[1]) * te;
|
||||
c3 = lo[2] + (hi[2] - lo[2]) * te;
|
||||
if (p->patch_amount > 0) {
|
||||
double pn = fbm(x + ox + 31.7, y + oy - 12.2, z + oz + 5.5, 4, p->patch_frequency, 0.5, 2);
|
||||
double w = smoothstep(-0.05, 0.4, pn) * p->patch_amount * (1 - e * 0.6);
|
||||
c1 = c1 + (p->patch[0] - c1) * w;
|
||||
c2 = c2 + (p->patch[1] - c2) * w;
|
||||
c3 = c3 + (p->patch[2] - c3) * w;
|
||||
}
|
||||
gg = g->ejecta[i] * p->crater_ejecta;
|
||||
c1 = c1 + (p->high[0] - c1) * gg;
|
||||
c2 = c2 + (p->high[1] - c2) * gg;
|
||||
c3 = c3 + (p->high[2] - c3) * gg;
|
||||
steepness = 1 - (g->normals[o] * x + g->normals[o + 1] * y + g->normals[o + 2] * z);
|
||||
ws = smoothstep(p->steep_threshold - p->steep_blend, p->steep_threshold + p->steep_blend, steepness);
|
||||
c1 = c1 + (p->steep[0] - c1) * ws;
|
||||
c2 = c2 + (p->steep[1] - c2) * ws;
|
||||
c3 = c3 + (p->steep[2] - c3) * ws;
|
||||
gr = 1 + fbm(x + ox, y + oy, z + oz, 2, 14, 0.5, 2) * p->grain;
|
||||
g->colors[o] = c1 * gr;
|
||||
g->colors[o + 1] = c2 * gr;
|
||||
g->colors[o + 2] = c3 * gr;
|
||||
}
|
||||
|
||||
int mt_step(mt_gen* g, double budget)
|
||||
{
|
||||
double deadline = clock_secs() + budget;
|
||||
int count = g->count, every = 0;
|
||||
#define DUE() (budget > 0 && (++every & 31) == 0 && clock_secs() > deadline)
|
||||
while (g->stage != ST_DONE) {
|
||||
switch (g->stage) {
|
||||
case ST_DIRS:
|
||||
while (g->i < count) {
|
||||
dir_at(g, g->i++);
|
||||
if (DUE())
|
||||
goto out;
|
||||
}
|
||||
g->stage = ST_CRATERS;
|
||||
break;
|
||||
case ST_CRATERS:
|
||||
if (!make_craters(g)) {
|
||||
g->stage = ST_DONE; /* (out of memory: a flat moon) */
|
||||
break;
|
||||
}
|
||||
g->stage = ST_HEIGHTS;
|
||||
g->i = 0;
|
||||
break;
|
||||
case ST_HEIGHTS:
|
||||
while (g->i < count) {
|
||||
height_at(g, g->i++);
|
||||
if (DUE())
|
||||
goto out;
|
||||
}
|
||||
g->stage = ST_MEAN;
|
||||
break;
|
||||
case ST_MEAN: {
|
||||
double mean = 0;
|
||||
int i;
|
||||
for (i = 0; i < count; ++i)
|
||||
mean = mean + g->heights[i];
|
||||
g->mean = mean / count;
|
||||
g->min_h = HUGE_VAL;
|
||||
g->max_h = -HUGE_VAL;
|
||||
g->stage = ST_POSITIONS;
|
||||
g->i = 0;
|
||||
break;
|
||||
}
|
||||
case ST_POSITIONS: {
|
||||
int i;
|
||||
for (i = 0; i < count; ++i) {
|
||||
double hh = g->heights[i] - g->mean, r;
|
||||
g->heights[i] = hh;
|
||||
r = 1 + hh;
|
||||
g->positions[i * 3] = g->dirs[i * 3] * r;
|
||||
g->positions[i * 3 + 1] = g->dirs[i * 3 + 1] * r;
|
||||
g->positions[i * 3 + 2] = g->dirs[i * 3 + 2] * r;
|
||||
if (hh < g->min_h)
|
||||
g->min_h = hh;
|
||||
if (hh > g->max_h)
|
||||
g->max_h = hh;
|
||||
}
|
||||
g->stage = ST_NORMALS;
|
||||
g->i = 0;
|
||||
break;
|
||||
}
|
||||
case ST_NORMALS: {
|
||||
/* g->i: the row (face * n + y) */
|
||||
int n = g->n, rows = 6 * n;
|
||||
while (g->i < rows) {
|
||||
int f = g->i / n, y = g->i % n, x;
|
||||
int base = f * g->per;
|
||||
for (x = 0; x < n; ++x) {
|
||||
int i = base + y * g->row + x;
|
||||
tri(g, i, i + 1, i + n + 2);
|
||||
tri(g, i, i + n + 2, i + n + 1);
|
||||
}
|
||||
++g->i;
|
||||
if (budget > 0 && clock_secs() > deadline)
|
||||
goto out;
|
||||
}
|
||||
g->stage = ST_SEAMS;
|
||||
break;
|
||||
}
|
||||
case ST_SEAMS:
|
||||
seams(g);
|
||||
g->stage = ST_NORMALIZE;
|
||||
break;
|
||||
case ST_NORMALIZE: {
|
||||
int i;
|
||||
for (i = 0; i < count; ++i) {
|
||||
int o = i * 3;
|
||||
double nx = g->normals[o], ny = g->normals[o + 1], nz = g->normals[o + 2];
|
||||
double len = sqrt(nx * nx + ny * ny + nz * nz);
|
||||
if (len == 0)
|
||||
len = 1;
|
||||
g->normals[o] = nx / len;
|
||||
g->normals[o + 1] = ny / len;
|
||||
g->normals[o + 2] = nz / len;
|
||||
}
|
||||
g->stage = ST_COLORS;
|
||||
g->i = 0;
|
||||
break;
|
||||
}
|
||||
case ST_COLORS:
|
||||
while (g->i < count) {
|
||||
color_at(g, g->i++);
|
||||
if (DUE())
|
||||
goto out;
|
||||
}
|
||||
g->stage = ST_DONE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
out:
|
||||
#undef DUE
|
||||
return g->stage == ST_DONE;
|
||||
}
|
||||
|
||||
int mt_n(const mt_gen* g) { return g->n; }
|
||||
int mt_count(const mt_gen* g) { return g->count; }
|
||||
const double* mt_positions(const mt_gen* g) { return g->positions; }
|
||||
const double* mt_normals(const mt_gen* g) { return g->normals; }
|
||||
const double* mt_colors(const mt_gen* g) { return g->colors; }
|
||||
double mt_min_h(const mt_gen* g) { return g->min_h; }
|
||||
double mt_max_h(const mt_gen* g) { return g->max_h; }
|
||||
|
||||
int mt_strip_len(int n)
|
||||
{
|
||||
/* per face n rows of 2 (n + 1), joined by 2 repeated vertices (none before the first) */
|
||||
return 6 * n * 2 * (n + 1) + 2 * (6 * n - 1);
|
||||
}
|
||||
|
||||
void mt_strip(int n, int* out)
|
||||
{
|
||||
int row = n + 1, per = row * row, c = 0, f, j, i;
|
||||
for (f = 0; f < 6; ++f) {
|
||||
int base = f * per;
|
||||
for (j = 0; j < n; ++j) {
|
||||
if (c > 0) {
|
||||
out[c] = out[c - 1];
|
||||
out[c + 1] = base + (j + 1) * row;
|
||||
c += 2;
|
||||
}
|
||||
for (i = 0; i <= n; ++i) {
|
||||
out[c] = base + (j + 1) * row + i;
|
||||
out[c + 1] = base + j * row + i;
|
||||
c += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mt_fill(const mt_gen* g, float* pos, float* nor, float* col, int stride, int from, int to)
|
||||
{
|
||||
int n = g->n, row = n + 1, per = row * row, k = 0, f, j, i, last = -1;
|
||||
/* the strip of mt_strip, walked without the index array */
|
||||
#define PUT(v) \
|
||||
do { \
|
||||
int v3 = (v) * 3; \
|
||||
if (k < from || k >= to) { \
|
||||
last = (v); \
|
||||
++k; \
|
||||
break; \
|
||||
} \
|
||||
float* pp = pos ? pos + (size_t)k * stride : 0; \
|
||||
float* nn = nor ? nor + (size_t)k * stride : 0; \
|
||||
float* cc = col ? col + (size_t)k * stride : 0; \
|
||||
if (pp) { pp[0] = (float)g->positions[v3]; pp[1] = (float)g->positions[v3 + 1]; pp[2] = (float)g->positions[v3 + 2]; } \
|
||||
if (nn) { nn[0] = (float)g->normals[v3]; nn[1] = (float)g->normals[v3 + 1]; nn[2] = (float)g->normals[v3 + 2]; } \
|
||||
if (cc) { cc[0] = (float)g->colors[v3]; cc[1] = (float)g->colors[v3 + 1]; cc[2] = (float)g->colors[v3 + 2]; } \
|
||||
last = (v); \
|
||||
++k; \
|
||||
} while (0)
|
||||
for (f = 0; f < 6; ++f) {
|
||||
int base = f * per;
|
||||
for (j = 0; j < n; ++j) {
|
||||
if (k > 0) {
|
||||
int prev = last;
|
||||
PUT(prev);
|
||||
PUT(base + (j + 1) * row);
|
||||
}
|
||||
for (i = 0; i <= n; ++i) {
|
||||
PUT(base + (j + 1) * row + i);
|
||||
PUT(base + j * row + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
#undef PUT
|
||||
}
|
||||
|
||||
#if MT_THREADS
|
||||
static void* run_thread(void* arg)
|
||||
{
|
||||
mt_gen* g = (mt_gen*)arg;
|
||||
mt_step(g, 0);
|
||||
__atomic_store_n(&g->finished, 1, __ATOMIC_RELEASE);
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
int mt_start(mt_gen* g)
|
||||
{
|
||||
#if MT_THREADS
|
||||
if (g->threaded || g->stage == ST_DONE)
|
||||
return g->threaded;
|
||||
if (pthread_create(&g->thread, 0, run_thread, g) != 0)
|
||||
return 0;
|
||||
g->threaded = 1;
|
||||
return 1;
|
||||
#else
|
||||
(void)g;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
int mt_done(mt_gen* g)
|
||||
{
|
||||
#if MT_THREADS
|
||||
if (g->threaded)
|
||||
return __atomic_load_n(&g->finished, __ATOMIC_ACQUIRE);
|
||||
#endif
|
||||
return g->stage == ST_DONE;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/* The moon generator in C: a port of the server's web/static/js/terrain.js generation.
|
||||
* Same random draws (mulberry32, the simplex permutation) and the same arithmetic in the
|
||||
* same order, in doubles, so the output matches terrain.js within its Float32Array
|
||||
* rounding.
|
||||
*
|
||||
* Plain C, no engine or Lua headers: mmterrain.c binds it for the engine, and
|
||||
* scripts/moon-check.mjs loads it into luajit through the FFI (a host build) to compare
|
||||
* it with terrain.js. The work runs in steps with a time budget (mt_step), so a slow
|
||||
* target (wasm) can spread it over frames.
|
||||
*/
|
||||
#ifndef MM_TERRAIN_H
|
||||
#define MM_TERRAIN_H
|
||||
|
||||
#ifndef MT_THREADS
|
||||
#if defined(__EMSCRIPTEN__) || defined(_WIN32)
|
||||
#define MT_THREADS 0
|
||||
#else
|
||||
#define MT_THREADS 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Techniques, in terrain.lua's TECHNIQUES order. */
|
||||
enum { MT_FBM = 0, MT_RIDGED = 1, MT_WARPED = 2, MT_BILLOW = 3, MT_CRATER_FIELD = 4 };
|
||||
|
||||
/* The resolved parameters of one moon (terrain.lua resolve), colors already linear RGB. */
|
||||
typedef struct mt_params {
|
||||
double resolution;
|
||||
int technique;
|
||||
double shape_strength, shape_frequency, shape_octaves, shape_persistence, shape_lacunarity, shape_min;
|
||||
double ridge_strength, ridge_frequency, ridge_octaves, ridge_persistence, ridge_lacunarity,
|
||||
ridge_sharpness, ridge_gain, ridge_min;
|
||||
double warp_strength, warp_frequency, warp_octaves;
|
||||
double crater_count, crater_min_radius, crater_max_radius, crater_size_bias, crater_depth,
|
||||
crater_rim_width, crater_rim_steepness, crater_floor, crater_smoothness, crater_ejecta;
|
||||
double low[3], mid[3], high[3], steep[3], patch[3];
|
||||
double steep_threshold, steep_blend, patch_amount, patch_frequency, grain;
|
||||
double offset[3];
|
||||
double crater_seed;
|
||||
} mt_params;
|
||||
|
||||
typedef struct mt_gen mt_gen;
|
||||
|
||||
/* A generator for p (copied). NULL when out of memory. */
|
||||
mt_gen* mt_new(const mt_params* p);
|
||||
void mt_free(mt_gen* g);
|
||||
/* Works for about `budget` seconds (<= 0: until done); returns 1 when done. */
|
||||
int mt_step(mt_gen* g, double budget);
|
||||
/* Where there are threads (desktop: MT_THREADS), runs the whole generation on a thread of
|
||||
* its own and returns 1 (then only mt_done and, once done, the results may be used);
|
||||
* else returns 0 (step it instead). */
|
||||
int mt_start(mt_gen* g);
|
||||
/* 1 once the generation is complete (threaded or stepped). */
|
||||
int mt_done(mt_gen* g);
|
||||
/* The grid size, its vertex count, and (when done) the flat xyz arrays over the grid
|
||||
* (vertex v at [3v..3v+2]) and the height range, like terrain.js generateMoon's. */
|
||||
int mt_n(const mt_gen* g);
|
||||
int mt_count(const mt_gen* g);
|
||||
const double* mt_positions(const mt_gen* g);
|
||||
const double* mt_normals(const mt_gen* g);
|
||||
const double* mt_colors(const mt_gen* g);
|
||||
double mt_min_h(const mt_gen* g);
|
||||
double mt_max_h(const mt_gen* g);
|
||||
|
||||
/* The triangle strip over the grid (terrain.lua strip): its length, and its 0-based grid
|
||||
* indices into out (mt_strip_len(n) ints). */
|
||||
int mt_strip_len(int n);
|
||||
void mt_strip(int n, int* out);
|
||||
/* Writes the strip's positions, normals and colors as floats, its vertices k in
|
||||
* [from, to): vertex k at pos[k * stride ..], nor[k * stride ..], col[k * stride ..] (any
|
||||
* may be NULL). */
|
||||
void mt_fill(const mt_gen* g, float* pos, float* nor, float* col, int stride, int from, int to);
|
||||
|
||||
/* The parameters from a flat array in terrain.lua native_params order (MT_PARAM_COUNT
|
||||
* numbers); returns 0 when it is too short. */
|
||||
#define MT_PARAM_COUNT 53
|
||||
int mt_params_from_array(const double* a, int n, mt_params* p);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,428 @@
|
||||
-- Blocks: parts of the layout that a rebuild keeps when what they show hasn't changed.
|
||||
-- Every new state rebuilds the screen (core/main.lua); a block is built once into a
|
||||
-- container node and kept, with its nodes and Druid components, by the next builds while
|
||||
-- its signature (M.sig of everything it shows) stays the same. Regions are blocks
|
||||
-- (header, moon list, main panel, aside), and so are costly parts inside them (the
|
||||
-- market's chart, ticker and sparklines, cards).
|
||||
--
|
||||
-- local b, kept = blocks.open(g, key, sig, parent) -- build into b.root unless kept
|
||||
-- if not kept then ... build into b.root ... end
|
||||
-- local ret = blocks.close(g, b, ...) -- what it returns, kept or not
|
||||
-- -- or: blocks.keep(g, key, sig, parent, function(root) ... return ... end)
|
||||
--
|
||||
-- Build registrations (test ids and keyboard focus, tooltips, tickers, fields, scroll
|
||||
-- areas, effect anchors, ...) are reset by every build; those made while a block builds
|
||||
-- are noted (M.note) and replayed in their order when it's kept, so a kept block is in
|
||||
-- every registry like a rebuilt one. Druid components made while a block builds are its
|
||||
-- own: they go when it goes (the screen's Druid instance lives on; core/main.lua).
|
||||
-- A keyed block inside one that is rebuilt survives it (set aside until the new build
|
||||
-- asks for it); blocks no build asked for go at the end of the build (M.finish).
|
||||
|
||||
local helper = require("druid.helper")
|
||||
|
||||
local M = {}
|
||||
|
||||
local stack = {} -- blocks being built, innermost last
|
||||
local replaying = false
|
||||
local orphans = {} -- components made outside any block's build (not a region's)
|
||||
|
||||
local function pack(...)
|
||||
return { n = select("#", ...), ... }
|
||||
end
|
||||
|
||||
local function alive(node)
|
||||
return node ~= nil and pcall(gui.get_id, node)
|
||||
end
|
||||
|
||||
---------------------------------------------------------------- signatures
|
||||
|
||||
local function ser(v, buf)
|
||||
local t = type(v)
|
||||
if t == "table" then
|
||||
buf[#buf + 1] = "{"
|
||||
local n = #v
|
||||
for i = 1, n do
|
||||
ser(v[i], buf)
|
||||
buf[#buf + 1] = ","
|
||||
end
|
||||
local keys, mixed
|
||||
for k in pairs(v) do
|
||||
if type(k) ~= "number" or k < 1 or k > n or k % 1 ~= 0 then
|
||||
keys = keys or {}
|
||||
keys[#keys + 1] = k
|
||||
mixed = mixed or type(k) ~= "string"
|
||||
end
|
||||
end
|
||||
if keys then
|
||||
if mixed then
|
||||
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
|
||||
else
|
||||
table.sort(keys)
|
||||
end
|
||||
for _, k in ipairs(keys) do
|
||||
buf[#buf + 1] = tostring(k)
|
||||
buf[#buf + 1] = "="
|
||||
ser(v[k], buf)
|
||||
buf[#buf + 1] = ";"
|
||||
end
|
||||
end
|
||||
buf[#buf + 1] = "}"
|
||||
elseif t == "string" then
|
||||
buf[#buf + 1] = #v .. ":" .. v
|
||||
elseif t == "function" then
|
||||
buf[#buf + 1] = "f"
|
||||
else
|
||||
buf[#buf + 1] = tostring(v)
|
||||
end
|
||||
end
|
||||
|
||||
-- A string that is the same exactly when the values are (tables by content, keys sorted).
|
||||
function M.sig(...)
|
||||
local buf = {}
|
||||
for i = 1, select("#", ...) do
|
||||
ser((select(i, ...)), buf)
|
||||
buf[#buf + 1] = "|"
|
||||
end
|
||||
return table.concat(buf)
|
||||
end
|
||||
|
||||
---------------------------------------------------------------- registrations
|
||||
|
||||
-- Note a registration made while building (fn(...) makes it again in a later build that
|
||||
-- keeps the block). The registry functions call this; it does nothing outside a block.
|
||||
function M.note(fn, ...)
|
||||
local b = stack[#stack]
|
||||
if replaying or not b then
|
||||
return
|
||||
end
|
||||
local j = b.journal
|
||||
j[#j + 1] = { fn = fn, n = select("#", ...), ... }
|
||||
end
|
||||
|
||||
-- t[field] (a list reset by every build) gets value, noted.
|
||||
local function push(t, field, value)
|
||||
local l = t[field]
|
||||
if not l then
|
||||
l = {}
|
||||
t[field] = l
|
||||
end
|
||||
l[#l + 1] = value
|
||||
end
|
||||
function M.push(t, field, value)
|
||||
push(t, field, value)
|
||||
M.note(push, t, field, value)
|
||||
end
|
||||
|
||||
-- t[field][k] = value (a table reset by every build), noted.
|
||||
local function put(t, field, k, value)
|
||||
local m = t[field]
|
||||
if not m then
|
||||
m = {}
|
||||
t[field] = m
|
||||
end
|
||||
m[k] = value
|
||||
end
|
||||
function M.put(t, field, k, value)
|
||||
put(t, field, k, value)
|
||||
M.note(put, t, field, k, value)
|
||||
end
|
||||
|
||||
local replay
|
||||
|
||||
-- A kept block inside the one being built: noted in it, its registrations made again.
|
||||
local function adopt(b)
|
||||
local top = stack[#stack]
|
||||
if top then
|
||||
top.journal[#top.journal + 1] = { block = b }
|
||||
end
|
||||
replay(b)
|
||||
end
|
||||
|
||||
replay = function(b)
|
||||
for _, e in ipairs(b.journal) do
|
||||
if e.block then
|
||||
if not e.block.dead then
|
||||
replay(e.block)
|
||||
end
|
||||
else
|
||||
replaying = true
|
||||
pcall(e.fn, unpack(e, 1, e.n)) -- (a node that went since: a list row scrolled out)
|
||||
replaying = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- fn, made to run inside the block being built now when it's called later, outside a
|
||||
-- build (a list making rows as it scrolls, a field's on_change building again): what it
|
||||
-- makes and registers is that block's.
|
||||
function M.bound(fn)
|
||||
local b = stack[#stack]
|
||||
if not b then
|
||||
return fn
|
||||
end
|
||||
return function(...)
|
||||
if #stack > 0 or b.dead then
|
||||
return fn(...)
|
||||
end
|
||||
stack[1] = b
|
||||
local r = pack(pcall(fn, ...))
|
||||
stack = {}
|
||||
if not r[1] then
|
||||
error(r[2], 0)
|
||||
end
|
||||
return unpack(r, 2, r.n)
|
||||
end
|
||||
end
|
||||
|
||||
---------------------------------------------------------------- components
|
||||
|
||||
-- Components of g.druid made while a block builds belong to it (core/main.lua hooks the
|
||||
-- instance's new once per instance).
|
||||
function M.hook(druid_instance)
|
||||
local new = druid_instance.new
|
||||
druid_instance.new = function(self, component, ...)
|
||||
local c = new(self, component, ...)
|
||||
if not c._meta.parent then -- (a widget's own components go with it)
|
||||
local b = stack[#stack]
|
||||
if b then
|
||||
b.comps[#b.comps + 1] = c
|
||||
else
|
||||
orphans[#orphans + 1] = c
|
||||
end
|
||||
end
|
||||
return c
|
||||
end
|
||||
end
|
||||
|
||||
-- After a kept block moved under another parent: Druid buttons and hovers clip their
|
||||
-- clicks to the stencil above them, found when they were made (perhaps deleted with the
|
||||
-- old parent; a deleted node still answers until the frame ends, so always look again).
|
||||
local function rezone(b)
|
||||
for _, c in ipairs(b.comps) do
|
||||
if c.node and c.set_click_zone and (c.hover or c.on_mouse_hover) then
|
||||
pcall(c.set_click_zone, c, helper.get_closest_stencil_node(c.node))
|
||||
end
|
||||
end
|
||||
for _, e in ipairs(b.journal) do
|
||||
if e.block and not e.block.dead then
|
||||
rezone(e.block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- True while a kept block's registrations are made again (test.register looks for its
|
||||
-- clip zone again then: the one it was given may have gone with the old parent).
|
||||
function M.replaying()
|
||||
return replaying
|
||||
end
|
||||
|
||||
local function node_of(c)
|
||||
return c.node or c.root or c.view_node
|
||||
end
|
||||
|
||||
---------------------------------------------------------------- lifetime
|
||||
|
||||
local function blocks_of(g)
|
||||
g.blocks = g.blocks or { by_key = {}, aside = {}, used = {} }
|
||||
return g.blocks
|
||||
end
|
||||
|
||||
-- Taken by the build going on (kept or made again under a key).
|
||||
local function taken(reg, b)
|
||||
return b.key ~= nil and reg.used[b.key] and reg.by_key[b.key] == b
|
||||
end
|
||||
|
||||
local destroy
|
||||
|
||||
-- Removes a block: keyed blocks inside it are set aside (detached, still whole) for the
|
||||
-- build going on to take back; the rest goes.
|
||||
destroy = function(g, b, set_aside)
|
||||
local reg = blocks_of(g)
|
||||
for _, e in ipairs(b.journal) do
|
||||
local c = e.block
|
||||
if c and not c.dead and not taken(reg, c) then
|
||||
if c.key and set_aside and alive(c.root) then
|
||||
gui.set_parent(c.root, reg.limbo, false)
|
||||
reg.aside[#reg.aside + 1] = c
|
||||
else
|
||||
destroy(g, c, set_aside)
|
||||
end
|
||||
end
|
||||
end
|
||||
g.tearing_down = true -- (components going away fire events: core/list, scroll)
|
||||
for i = #b.comps, 1, -1 do
|
||||
pcall(g.druid.remove, g.druid, b.comps[i])
|
||||
end
|
||||
g.tearing_down = false
|
||||
b.comps = {}
|
||||
if alive(b.root) then
|
||||
gui.delete_node(b.root)
|
||||
end
|
||||
if b.key and reg.by_key[b.key] == b then
|
||||
reg.by_key[b.key] = nil
|
||||
end
|
||||
b.dead = true
|
||||
end
|
||||
|
||||
-- Components made outside blocks whose nodes went (the moon list's search filtering
|
||||
-- rows again, ...).
|
||||
local function sweep(g)
|
||||
local keep = {}
|
||||
for _, c in ipairs(orphans) do
|
||||
local n = node_of(c)
|
||||
if n and not alive(n) then
|
||||
pcall(g.druid.remove, g.druid, c)
|
||||
elseif n then
|
||||
keep[#keep + 1] = c
|
||||
end
|
||||
end
|
||||
orphans = keep
|
||||
end
|
||||
|
||||
-- Start of a build. full: nothing is kept (core/main.lua makes the layout and the Druid
|
||||
-- instance again).
|
||||
function M.begin(g, full)
|
||||
local reg = blocks_of(g)
|
||||
stack = {}
|
||||
replaying = false
|
||||
if full then
|
||||
reg.by_key, reg.aside = {}, {}
|
||||
orphans = {}
|
||||
end
|
||||
reg.used = {}
|
||||
if not alive(reg.limbo) then
|
||||
reg.limbo = gui.new_box_node(vmath.vector3(0, 0, 0), vmath.vector3(1, 1, 0))
|
||||
gui.set_enabled(reg.limbo, false)
|
||||
end
|
||||
end
|
||||
|
||||
-- Open a block under parent. Returns the block (build into b.root) and whether it was
|
||||
-- kept: the last one with this key had the same sig. key nil: never kept (still a
|
||||
-- block, so keyed blocks inside it can be).
|
||||
function M.open(g, key, sig, parent)
|
||||
local reg = blocks_of(g)
|
||||
if key and reg.used[key] then
|
||||
key = nil -- (asked for twice in one build: the second isn't kept)
|
||||
end
|
||||
local old = key and reg.by_key[key]
|
||||
if old and old.dead then
|
||||
old = nil
|
||||
end
|
||||
if old and old.sig == sig and alive(old.root) then
|
||||
reg.used[key] = true
|
||||
gui.set_parent(old.root, parent, false)
|
||||
rezone(old)
|
||||
adopt(old)
|
||||
return old, true
|
||||
end
|
||||
if old then
|
||||
destroy(g, old, true)
|
||||
end
|
||||
local root = gui.new_box_node(vmath.vector3(0, 0, 0), vmath.vector3(1, 1, 0))
|
||||
gui.set_pivot(root, gui.PIVOT_NW)
|
||||
gui.set_parent(root, parent, false)
|
||||
gui.set_visible(root, false) -- (only a container: its children draw)
|
||||
local b = { key = key, sig = sig, root = root, journal = {}, comps = {} }
|
||||
if key then
|
||||
reg.by_key[key] = b
|
||||
reg.used[key] = true
|
||||
end
|
||||
local top = stack[#stack]
|
||||
if top then
|
||||
top.journal[#top.journal + 1] = { block = b }
|
||||
end
|
||||
stack[#stack + 1] = b
|
||||
return b, false
|
||||
end
|
||||
|
||||
-- The block open() would keep (same key and sig), without taking it; nil if none.
|
||||
function M.peek(g, key, sig)
|
||||
local reg = blocks_of(g)
|
||||
if not key or reg.used[key] then
|
||||
return nil
|
||||
end
|
||||
local b = reg.by_key[key]
|
||||
if b and not b.dead and b.sig == sig and alive(b.root) then
|
||||
return b
|
||||
end
|
||||
end
|
||||
|
||||
-- Close block b: ... is what its build returns; returns that (kept: what the build that
|
||||
-- made it returned).
|
||||
function M.close(g, b, ...)
|
||||
for i = #stack, 1, -1 do
|
||||
if stack[i] == b then
|
||||
for j = #stack, i, -1 do
|
||||
stack[j] = nil
|
||||
end
|
||||
b.ret = { n = select("#", ...), ... }
|
||||
return ...
|
||||
end
|
||||
end
|
||||
if b.ret then
|
||||
return unpack(b.ret, 1, b.ret.n)
|
||||
end
|
||||
end
|
||||
|
||||
-- Open b again to add to it (a card's height, known after its row): what is made until
|
||||
-- close() is its.
|
||||
function M.reopen(g, b)
|
||||
stack[#stack + 1] = b
|
||||
end
|
||||
|
||||
-- Drop block b now (a kept card that has to be built again).
|
||||
function M.drop(g, b)
|
||||
local reg = blocks_of(g)
|
||||
if b.key and reg.used[b.key] and reg.by_key[b.key] == b then
|
||||
reg.used[b.key] = nil
|
||||
end
|
||||
if not b.dead then
|
||||
destroy(g, b, false)
|
||||
end
|
||||
end
|
||||
|
||||
-- open + build + close: fn(root) builds; returns what it returns (and that again while
|
||||
-- the block is kept).
|
||||
function M.keep(g, key, sig, parent, fn)
|
||||
local b, kept = M.open(g, key, sig, parent)
|
||||
if kept then
|
||||
return M.close(g, b)
|
||||
end
|
||||
return M.close(g, b, fn(b.root))
|
||||
end
|
||||
|
||||
-- Destroy a keyed block now (a region not shown this build).
|
||||
function M.remove(g, key)
|
||||
local reg = blocks_of(g)
|
||||
local b = reg.by_key[key]
|
||||
if b then
|
||||
destroy(g, b, true)
|
||||
end
|
||||
end
|
||||
|
||||
-- End of a build: blocks it didn't ask for go.
|
||||
function M.finish(g)
|
||||
local reg = blocks_of(g)
|
||||
stack = {}
|
||||
for k, b in pairs(reg.by_key) do
|
||||
if not reg.used[k] then
|
||||
destroy(g, b, true)
|
||||
end
|
||||
end
|
||||
for _, b in ipairs(reg.aside) do
|
||||
if not b.dead and not taken(reg, b) then
|
||||
destroy(g, b, false)
|
||||
end
|
||||
end
|
||||
reg.aside = {}
|
||||
sweep(g)
|
||||
end
|
||||
|
||||
-- Forget every block (the screen goes away, or before a full rebuild deletes the layout).
|
||||
function M.clear(g)
|
||||
local reg = blocks_of(g)
|
||||
reg.by_key, reg.aside, reg.used = {}, {}, {}
|
||||
stack, orphans = {}, {}
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -6,6 +6,7 @@
|
||||
local ui = require("modules.ui")
|
||||
local theme = require("modules.theme")
|
||||
local test = require("modules.test")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -53,9 +54,8 @@ function M.new(g, parent, key, x, y, w, h, opts)
|
||||
end
|
||||
local input = g.druid:new_input(box, txt, opts.keyboard or gui.KEYBOARD_TYPE_DEFAULT)
|
||||
test.register("field:" .. key, box, "field", opts.hint)
|
||||
g.inputs = g.inputs or {} -- (core/keys: Enter submits; other keys skip while editing)
|
||||
g.inputs[#g.inputs + 1] = { input = input, submit = opts.on_submit, step = { min = opts.min, max = opts.max },
|
||||
esc_clears = opts.esc_clears }
|
||||
blocks.push(g, "inputs", { input = input, submit = opts.on_submit, step = { min = opts.min, max = opts.max },
|
||||
esc_clears = opts.esc_clears })
|
||||
-- (no "<" or "&": the text node would read them as markup)
|
||||
input:set_allowed_characters(opts.allowed or "[^<&]")
|
||||
if opts.max_len then
|
||||
|
||||
@@ -203,7 +203,7 @@ local function into_view(g, e)
|
||||
to = math.max(0, math.min(a.max or 0, to))
|
||||
if to ~= pos then
|
||||
g.scroll_pos[a.key] = to
|
||||
g.dirty = true
|
||||
g.dirty, g.dirty_all = true, true
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -266,7 +266,7 @@ local function list_step(g, f, cur, e, dir, scope_)
|
||||
to = math.max(0, math.min(a.max or 0, to))
|
||||
if to ~= pos then
|
||||
g.scroll_pos[a.key] = to
|
||||
g.dirty = true
|
||||
g.dirty, g.dirty_all = true, true
|
||||
end
|
||||
local now = find(g, key)
|
||||
f.tip = now and tooltip.show_for(now.node, M.rect(now.node)) or false
|
||||
|
||||
@@ -29,6 +29,7 @@ local ui = require("modules.ui")
|
||||
local theme = require("modules.theme")
|
||||
local sfx = require("modules.sfx")
|
||||
local anim = require("modules.anim")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local M = {}
|
||||
M.__index = M
|
||||
@@ -102,15 +103,13 @@ end
|
||||
-- A box an action's sweep covers when its button is inside (fx.js: the button's .card,
|
||||
-- .line, row or heading): cards and list rows register while they're built.
|
||||
function M:box(node)
|
||||
self.boxes = self.boxes or {}
|
||||
self.boxes[#self.boxes + 1] = node
|
||||
blocks.push(self, "boxes", node)
|
||||
end
|
||||
|
||||
-- An action button of this build by its test id (H.action): effects that come after a
|
||||
-- rebuild find the new node (the error shake, the ready burst).
|
||||
function M:button(id, node)
|
||||
self.buttons = self.buttons or {}
|
||||
self.buttons[id] = node
|
||||
blocks.put(self, "buttons", id, node)
|
||||
end
|
||||
|
||||
-- The layout rect {x, y, w, h} (x right, y down, from the page's top-left) of a node with
|
||||
@@ -193,8 +192,8 @@ end
|
||||
|
||||
-- A header number node for `key` (box: its chip or frame, the target for particles).
|
||||
function M:bind(key, node, box)
|
||||
self.binds[key] = { node = node, box = box, color = gui.get_color(node), p = gui.get_position(node),
|
||||
scale = gui.get_scale(node), w = ui.text_width(node) }
|
||||
blocks.put(self, "binds", key, { node = node, box = box, color = gui.get_color(node), p = gui.get_position(node),
|
||||
scale = gui.get_scale(node), w = ui.text_width(node) })
|
||||
end
|
||||
|
||||
local function drop_halo(c)
|
||||
@@ -740,7 +739,7 @@ function M:reticle(root, w, h, name)
|
||||
gui.set_pivot(glow, gui.PIVOT_CENTER)
|
||||
gui.set_blend_mode(glow, theme.glow_blend())
|
||||
gui.move_below(glow, parts[1])
|
||||
self.reticles[#self.reticles + 1] = { node = ret, w = w, h = h, name = name, arms = arms, ticks = ticks, glow = glow }
|
||||
blocks.push(self, "reticles", { node = ret, w = w, h = h, name = name, arms = arms, ticks = ticks, glow = glow })
|
||||
if self.shine_known and not self.shine_known[name] then
|
||||
-- It appeared shining: the reticle locks on after the tab's entrance.
|
||||
self.shine_known[name] = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- The moon in the aside viewport: the procedural moon of the web build (terrain.lua, a
|
||||
-- The moon in the aside viewport: the procedural moon of the web build (mmterrain and terrain.lua, a
|
||||
-- port of the server's terrain.js; look of scene.js), drawn in 3D behind a GUI node.
|
||||
--
|
||||
-- Use from a gui_script:
|
||||
@@ -13,12 +13,14 @@
|
||||
-- appearing in front of the moon shown) charges up, then bursts
|
||||
-- into moon `id` once its mesh is built. moon.show(id, node) during
|
||||
-- it only moves the viewport. Progress in moon.revealing (below).
|
||||
-- moon.prefetch(id) start making moon `id`'s mesh ahead (a bought moon, before its
|
||||
-- reveal is shown).
|
||||
-- moon.skip_reveal() jump ahead: a charge bursts as soon as the moon is ready, a
|
||||
-- burst ends.
|
||||
--
|
||||
-- How it works: moon/moon.script (in moon/moon.collection, a collection instance in the
|
||||
-- game screen) generates meshes (cached per id, spread over frames), spins and orbits the
|
||||
-- camera (drag to rotate, wheel to zoom), and fills `moon.frame`; the render script
|
||||
-- game screen) generates meshes (cached per id; natively through the mmterrain extension:
|
||||
-- on a thread on desktop, a few ms a frame on the web), spins and orbits the camera (drag to rotate, wheel to zoom), and fills `moon.frame`; the render script
|
||||
-- (render/mm.render_script) draws the scene into an HDR target and binds it as
|
||||
-- `moon_tex` for the GUI node's material, which tone maps it (ACES, like three.js).
|
||||
--
|
||||
@@ -126,6 +128,17 @@ function M.reveal(id, node)
|
||||
set_node(node)
|
||||
end
|
||||
|
||||
-- Start making moon `id`'s mesh now (a moon just bought, before its reveal shows it), so
|
||||
-- it's ready when asked for.
|
||||
function M.prefetch(id)
|
||||
if debug_id ~= "" then
|
||||
id = debug_id
|
||||
end
|
||||
if id ~= nil and tostring(id) ~= M.id then
|
||||
M.prefetch_id = tostring(id)
|
||||
end
|
||||
end
|
||||
|
||||
function M.skip_reveal()
|
||||
local r = M.revealing
|
||||
if r then
|
||||
|
||||
+30
-445
@@ -1,7 +1,7 @@
|
||||
-- Procedural moon terrain: a port of the server's web/static/js/terrain.js (generation,
|
||||
-- deterministic from the moon id). Same seed and settings give the same moon as the web:
|
||||
-- the integer hashing and random draws are bit-exact, the floats differ only by the JS
|
||||
-- Float32Array rounding (~1e-7).
|
||||
-- Moon terrain settings and helpers, a port of the server's web/static/js/terrain.js: the
|
||||
-- settings (defaults, merge, per-moon resolve, the description traits), the colors and
|
||||
-- the cube-sphere grid and its triangle strip. The generation itself is native: the
|
||||
-- mmterrain extension (mmterrain/src/terrain.c) takes native_params(resolve(...)).
|
||||
--
|
||||
-- Pure Lua (LuaJIT `bit`), no engine APIs, so it also runs in plain luajit for checks
|
||||
-- (scripts/moon-check.mjs). Keep it in sync with terrain.js when that changes.
|
||||
@@ -131,146 +131,6 @@ function M.rng(seed)
|
||||
end
|
||||
end
|
||||
|
||||
---------------------------------------------------------------- noise
|
||||
|
||||
-- 3D simplex noise (Stefan Gustavson's reference implementation), roughly in [-1, 1].
|
||||
-- 0-based tables (LuaJIT keeps index 0 in the array part). GX/GY/GZ[v] is the gradient of
|
||||
-- permutation value v (GRAD3 at v % 12, i.e. terrain.js PERM12).
|
||||
local GRAD3 = {
|
||||
1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, 0, 1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, -1, 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1,
|
||||
}
|
||||
local PERM, GX, GY, GZ = {}, {}, {}, {}
|
||||
do
|
||||
local p = {}
|
||||
for i = 0, 255 do
|
||||
p[i] = i
|
||||
end
|
||||
local r = M.rng(1337)
|
||||
for i = 255, 1, -1 do
|
||||
local j = floor(r() * (i + 1))
|
||||
p[i], p[j] = p[j], p[i]
|
||||
end
|
||||
for i = 0, 511 do
|
||||
PERM[i] = p[band(i, 255)]
|
||||
end
|
||||
for v = 0, 255 do
|
||||
local g = (v % 12) * 3
|
||||
GX[v], GY[v], GZ[v] = GRAD3[g + 1], GRAD3[g + 2], GRAD3[g + 3]
|
||||
end
|
||||
end
|
||||
M.PERM = PERM
|
||||
|
||||
local F3 = 1 / 3
|
||||
local G3 = 1 / 6
|
||||
|
||||
-- Branch-free (LuaJIT side traces of the reference's if/else chains abort on register
|
||||
-- pressure): the simplex corner order comes from 0/1 flags, and corners outside their
|
||||
-- radius get a zero weight. Bit-identical results: for floats, x >= y iff x - y >= 0, and
|
||||
-- here |x - y| < 1, so 1 + floor(x - y) is that flag.
|
||||
local function simplex3(x, y, z)
|
||||
local P = PERM
|
||||
local s = (x + y + z) * F3
|
||||
local i, j, k = floor(x + s), floor(y + s), floor(z + s)
|
||||
local t = (i + j + k) * G3
|
||||
local x0, y0, z0 = x - (i - t), y - (j - t), z - (k - t)
|
||||
local a = 1 + floor(x0 - y0) -- x0 >= y0
|
||||
local b = 1 + floor(y0 - z0) -- y0 >= z0
|
||||
local c = 1 + floor(x0 - z0) -- x0 >= z0
|
||||
local i1 = a * (b + (1 - b) * c)
|
||||
local j1 = (1 - a) * b
|
||||
local k1 = 1 - i1 - j1
|
||||
local i2 = a + (1 - a) * b * c
|
||||
local j2 = a * b + 1 - a
|
||||
local k2 = 2 - i2 - j2
|
||||
local ii, jj, kk = band(i, 255), band(j, 255), band(k, 255)
|
||||
local tt = max(0, 0.6 - x0 * x0 - y0 * y0 - z0 * z0)
|
||||
local g = P[ii + P[jj + P[kk]]]
|
||||
tt = tt * tt
|
||||
local n = tt * tt * (GX[g] * x0 + GY[g] * y0 + GZ[g] * z0)
|
||||
local x1, y1, z1 = x0 - i1 + G3, y0 - j1 + G3, z0 - k1 + G3
|
||||
tt = max(0, 0.6 - x1 * x1 - y1 * y1 - z1 * z1)
|
||||
g = P[ii + i1 + P[jj + j1 + P[kk + k1]]]
|
||||
tt = tt * tt
|
||||
n = n + tt * tt * (GX[g] * x1 + GY[g] * y1 + GZ[g] * z1)
|
||||
local x2, y2, z2 = x0 - i2 + 2 * G3, y0 - j2 + 2 * G3, z0 - k2 + 2 * G3
|
||||
tt = max(0, 0.6 - x2 * x2 - y2 * y2 - z2 * z2)
|
||||
g = P[ii + i2 + P[jj + j2 + P[kk + k2]]]
|
||||
tt = tt * tt
|
||||
n = n + tt * tt * (GX[g] * x2 + GY[g] * y2 + GZ[g] * z2)
|
||||
local x3, y3, z3 = x0 - 1 + 3 * G3, y0 - 1 + 3 * G3, z0 - 1 + 3 * G3
|
||||
tt = max(0, 0.6 - x3 * x3 - y3 * y3 - z3 * z3)
|
||||
g = P[ii + 1 + P[jj + 1 + P[kk + 1]]]
|
||||
tt = tt * tt
|
||||
n = n + tt * tt * (GX[g] * x3 + GY[g] * y3 + GZ[g] * z3)
|
||||
return 32 * n
|
||||
end
|
||||
M.simplex3 = simplex3
|
||||
|
||||
-- Caps on saved settings, which come from the admin page.
|
||||
local MAX_OCTAVES = 10
|
||||
local MAX_CRATERS = 2000
|
||||
|
||||
-- Fractal (fBm) noise in [-1, 1].
|
||||
local function fbm(x, y, z, octaves, frequency, persistence, lacunarity)
|
||||
local sum, amp, norm, f = 0, 1, 0, frequency
|
||||
for _ = 1, min(octaves, MAX_OCTAVES) do
|
||||
sum = sum + simplex3(x * f, y * f, z * f) * amp
|
||||
norm = norm + amp
|
||||
amp = amp * persistence
|
||||
f = f * lacunarity
|
||||
end
|
||||
return sum / norm
|
||||
end
|
||||
|
||||
-- Billow noise (fBm of |noise|) in [0, 1].
|
||||
local function billow(x, y, z, octaves, frequency, persistence, lacunarity)
|
||||
local sum, amp, norm, f = 0, 1, 0, frequency
|
||||
for _ = 1, min(octaves, MAX_OCTAVES) do
|
||||
sum = sum + abs(simplex3(x * f, y * f, z * f)) * amp
|
||||
norm = norm + amp
|
||||
amp = amp * persistence
|
||||
f = f * lacunarity
|
||||
end
|
||||
return sum / norm
|
||||
end
|
||||
|
||||
-- Ridged multifractal in [0, 1]: sharp crests; each octave is weighted by the previous.
|
||||
local function ridged(x, y, z, octaves, frequency, persistence, lacunarity, sharpness, gain)
|
||||
local sum, amp, norm, weight, f = 0, 1, 0, 1, frequency
|
||||
for _ = 1, min(octaves, MAX_OCTAVES) do
|
||||
local v = 1 - abs(simplex3(x * f, y * f, z * f))
|
||||
v = pow(v, sharpness) * weight
|
||||
weight = min(1, max(0, v * gain))
|
||||
sum = sum + v * amp
|
||||
norm = norm + amp
|
||||
amp = amp * persistence
|
||||
f = f * lacunarity
|
||||
end
|
||||
return sum / norm
|
||||
end
|
||||
|
||||
-- Polynomial smooth min/max (Inigo Quilez), k = smoothing width.
|
||||
local function smooth_min(a, b, k)
|
||||
if k <= 0 then
|
||||
return min(a, b)
|
||||
end
|
||||
local h = min(1, max(0, (b - a + k) / (2 * k)))
|
||||
return a * h + b * (1 - h) - k * h * (1 - h)
|
||||
end
|
||||
|
||||
local function smooth_max(a, b, k)
|
||||
return -smooth_min(-a, -b, k)
|
||||
end
|
||||
|
||||
local function smoothstep(e0, e1, x)
|
||||
local d = e1 - e0
|
||||
if d == 0 then
|
||||
d = 1e-6
|
||||
end
|
||||
local t = min(1, max(0, (x - e0) / d))
|
||||
return t * t * (3 - 2 * t)
|
||||
end
|
||||
|
||||
---------------------------------------------------------------- colors
|
||||
|
||||
local function round(x)
|
||||
@@ -410,6 +270,32 @@ function M.traits(settings, seed)
|
||||
}
|
||||
end
|
||||
|
||||
-- The resolved parameters as the flat number list the native generator takes
|
||||
-- (mmterrain/src/terrain.h mt_params_from_array; colors linear, like generate's).
|
||||
local TECHNIQUE_INDEX = { fbm = 0, ridged = 1, warped = 2, billow = 3, craterField = 4 }
|
||||
function M.native_params(p)
|
||||
local sh, rd, wp, cr, sd = p.shape, p.ridge, p.warp, p.craters, p.shading
|
||||
local off = p.offset or { 0, 0, 0 }
|
||||
local a = {
|
||||
p.resolution, TECHNIQUE_INDEX[p.technique] or 0,
|
||||
sh.strength, sh.frequency, sh.octaves, sh.persistence, sh.lacunarity, sh.minValue,
|
||||
rd.strength, rd.frequency, rd.octaves, rd.persistence, rd.lacunarity, rd.sharpness, rd.gain, rd.minValue,
|
||||
wp.strength, wp.frequency, wp.octaves,
|
||||
cr.count, cr.minRadius, cr.maxRadius, cr.sizeBias, cr.depth, cr.rimWidth, cr.rimSteepness,
|
||||
cr.floorHeight, cr.smoothness, cr.ejecta,
|
||||
}
|
||||
for _, key in ipairs({ "low", "mid", "high", "steep", "patch" }) do
|
||||
local c = M.linear_color(sd[key])
|
||||
a[#a + 1], a[#a + 2], a[#a + 3] = c[1], c[2], c[3]
|
||||
end
|
||||
local rest = { sd.steepThreshold, sd.steepBlend, sd.patchAmount, sd.patchFrequency, sd.grain,
|
||||
off[1], off[2], off[3], p.craterSeed or 1 }
|
||||
for _, v in ipairs(rest) do
|
||||
a[#a + 1] = v
|
||||
end
|
||||
return a
|
||||
end
|
||||
|
||||
---------------------------------------------------------------- mesh
|
||||
|
||||
-- Cube faces: outward normal and two tangent axes (a x b = up, so triangles face out).
|
||||
@@ -495,305 +381,4 @@ function M.strip(n)
|
||||
return out
|
||||
end
|
||||
|
||||
-- Craters scattered on the unit sphere, bucketed in a 3D grid for fast lookup.
|
||||
local function make_craters(c, seed, count_scale)
|
||||
local r = M.rng(seed)
|
||||
local count = round(min(c.count, MAX_CRATERS) * count_scale)
|
||||
local x_max = max(1 + c.rimWidth, sqrt(1 + max(0, c.smoothness)))
|
||||
local list = {}
|
||||
for i = 1, count do
|
||||
local z = r() * 2 - 1
|
||||
local a = r() * pi * 2
|
||||
local q = sqrt(1 - z * z)
|
||||
local radius = c.minRadius + (c.maxRadius - c.minRadius) * pow(r(), c.sizeBias)
|
||||
list[i] = { x = q * cos(a), y = q * sin(a), z = z, r = radius, reach = radius * x_max }
|
||||
end
|
||||
local G = 12
|
||||
local cell = 2 / G
|
||||
local grid = {}
|
||||
for i = 1, G * G * G do
|
||||
grid[i] = {}
|
||||
end
|
||||
local function at(v)
|
||||
return min(G - 1, max(0, floor((v + 1) / cell)))
|
||||
end
|
||||
for _, cr in ipairs(list) do
|
||||
for x = at(cr.x - cr.reach), at(cr.x + cr.reach) do
|
||||
for y = at(cr.y - cr.reach), at(cr.y + cr.reach) do
|
||||
for z = at(cr.z - cr.reach), at(cr.z + cr.reach) do
|
||||
local g = grid[(x * G + y) * G + z + 1]
|
||||
g[#g + 1] = cr
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return {
|
||||
cell_of = function(x, y, z)
|
||||
return grid[(at(x) * G + at(y)) * G + at(z) + 1]
|
||||
end,
|
||||
x_max = x_max,
|
||||
}
|
||||
end
|
||||
|
||||
-- Builds the moon for resolved parameters (see M.resolve): flat arrays over the cube-sphere
|
||||
-- grid (vertex v 0-based at [3v+1..3v+3]): positions (unit radius plus elevation), normals,
|
||||
-- colors (linear RGB). `checkpoint` (optional) is called every few hundred vertices, e.g.
|
||||
-- to yield a coroutine and spread the work over frames.
|
||||
function M.generate(p, checkpoint)
|
||||
checkpoint = checkpoint or function() end
|
||||
local n = max(8, min(320, round(p.resolution)))
|
||||
local row = n + 1
|
||||
local per = row * row
|
||||
local count = 6 * per
|
||||
local dirs, keys = M.cube_sphere(n)
|
||||
checkpoint()
|
||||
local heights, ejecta = {}, {}
|
||||
|
||||
local technique = p.technique
|
||||
local sh, rd, wp, cr = p.shape, p.ridge, p.warp, p.craters
|
||||
local off = p.offset or { 0, 0, 0 }
|
||||
local ox, oy, oz = off[1], off[2], off[3]
|
||||
local crater_field = technique == "craterField"
|
||||
local cparams = cr
|
||||
if crater_field then
|
||||
cparams = clone(cr)
|
||||
cparams.minRadius = cr.minRadius * 0.8
|
||||
cparams.maxRadius = cr.maxRadius * 0.6
|
||||
end
|
||||
local craters = make_craters(cparams, p.craterSeed or 1, crater_field and 3 or 1)
|
||||
checkpoint()
|
||||
local x_max = craters.x_max
|
||||
local shape_strength = crater_field and sh.strength * 0.4 or sh.strength
|
||||
local smooth = max(0, cr.smoothness)
|
||||
local ejecta_reach = 1 + cr.rimWidth * 2.5
|
||||
local rim_width, rim_steep, floor_h = cr.rimWidth, cr.rimSteepness, cr.floorHeight
|
||||
local s_oct, s_freq, s_pers, s_lac, s_min = sh.octaves, sh.frequency, sh.persistence, sh.lacunarity, sh.minValue
|
||||
local r_oct, r_freq, r_pers, r_lac, r_sharp, r_gain, r_min, r_str =
|
||||
rd.octaves, rd.frequency, rd.persistence, rd.lacunarity, rd.sharpness, rd.gain, rd.minValue, rd.strength
|
||||
|
||||
for i = 0, count - 1 do
|
||||
local x, y, z = dirs[i * 3 + 1], dirs[i * 3 + 2], dirs[i * 3 + 3]
|
||||
local px, py, pz = x + ox, y + oy, z + oz
|
||||
|
||||
-- Primary shape layer.
|
||||
local value
|
||||
if technique == "ridged" then
|
||||
value = ridged(px, py, pz, s_oct, s_freq, s_pers, s_lac, r_sharp, r_gain)
|
||||
elseif technique == "billow" then
|
||||
value = billow(px, py, pz, s_oct, s_freq, s_pers, s_lac)
|
||||
elseif technique == "warped" then
|
||||
local wf = wp.frequency
|
||||
local wx = fbm(px + 5.2, py + 1.3, pz, wp.octaves, wf, 0.5, 2)
|
||||
local wy = fbm(px, py + 9.7, pz + 2.8, wp.octaves, wf, 0.5, 2)
|
||||
local wz = fbm(px + 3.1, py, pz + 7.4, wp.octaves, wf, 0.5, 2)
|
||||
local s = wp.strength
|
||||
value = (fbm(px + wx * s, py + wy * s, pz + wz * s, s_oct, s_freq, s_pers, s_lac) + 1) / 2
|
||||
else
|
||||
value = (fbm(px, py, pz, s_oct, s_freq, s_pers, s_lac) + 1) / 2
|
||||
end
|
||||
local h = max(0, value - s_min) * shape_strength
|
||||
|
||||
-- Ridge layer.
|
||||
if r_str ~= 0 then
|
||||
local r = ridged(px - 11.3, py + 4.1, pz - 7.9, r_oct, r_freq, r_pers, r_lac, r_sharp, r_gain)
|
||||
h = h + max(0, r - r_min) * r_str
|
||||
end
|
||||
|
||||
-- Craters: parabola cavity, flattened floor, smooth rim (smooth min/max blend them).
|
||||
local crater_h, glow = 0, 0
|
||||
local list = craters.cell_of(x, y, z)
|
||||
for c = 1, #list do
|
||||
local k = list[c]
|
||||
local dx, dy, dz = x - k.x, y - k.y, z - k.z
|
||||
local d2 = dx * dx + dy * dy + dz * dz
|
||||
local reach = max(k.reach, k.r * ejecta_reach)
|
||||
if d2 <= reach * reach then
|
||||
local dist = sqrt(d2) / k.r
|
||||
if dist < x_max then
|
||||
local cavity = dist * dist - 1
|
||||
local rim_x = min(dist - 1 - rim_width, 0)
|
||||
local rim = rim_steep * rim_x * rim_x
|
||||
local shape = smooth_max(cavity, floor_h, smooth)
|
||||
shape = smooth_min(shape, rim, smooth)
|
||||
crater_h = crater_h + shape * k.r
|
||||
end
|
||||
if dist > 0.8 and dist < ejecta_reach then
|
||||
glow = glow + 1 - smoothstep(1, ejecta_reach, dist)
|
||||
end
|
||||
end
|
||||
end
|
||||
heights[i + 1] = h + crater_h * cr.depth
|
||||
ejecta[i + 1] = min(1, glow)
|
||||
if i % 64 == 0 then
|
||||
checkpoint()
|
||||
end
|
||||
end
|
||||
|
||||
-- Positions on a unit sphere plus elevation, re-centered so the mean radius stays 1.
|
||||
local mean = 0
|
||||
for i = 1, count do
|
||||
mean = mean + heights[i]
|
||||
if i % 4096 == 0 then
|
||||
checkpoint()
|
||||
end
|
||||
end
|
||||
mean = mean / count
|
||||
local positions = {}
|
||||
local min_h, max_h = math.huge, -math.huge
|
||||
for i = 0, count - 1 do
|
||||
local hh = heights[i + 1] - mean
|
||||
heights[i + 1] = hh
|
||||
local r = 1 + hh
|
||||
positions[i * 3 + 1] = dirs[i * 3 + 1] * r
|
||||
positions[i * 3 + 2] = dirs[i * 3 + 2] * r
|
||||
positions[i * 3 + 3] = dirs[i * 3 + 3] * r
|
||||
if hh < min_h then
|
||||
min_h = hh
|
||||
end
|
||||
if hh > max_h then
|
||||
max_h = hh
|
||||
end
|
||||
if i % 2048 == 0 then
|
||||
checkpoint()
|
||||
end
|
||||
end
|
||||
checkpoint()
|
||||
|
||||
-- Area-weighted vertex normals (the same two triangles per quad as terrain.js).
|
||||
local normals = {}
|
||||
for i = 1, count * 3 do
|
||||
normals[i] = 0
|
||||
if i % 8192 == 0 then
|
||||
checkpoint()
|
||||
end
|
||||
end
|
||||
local P = positions
|
||||
local function tri(a, b, c)
|
||||
a, b, c = a * 3, b * 3, c * 3
|
||||
local e1x, e1y, e1z = P[b + 1] - P[a + 1], P[b + 2] - P[a + 2], P[b + 3] - P[a + 3]
|
||||
local e2x, e2y, e2z = P[c + 1] - P[a + 1], P[c + 2] - P[a + 2], P[c + 3] - P[a + 3]
|
||||
local nx = e1y * e2z - e1z * e2y
|
||||
local ny = e1z * e2x - e1x * e2z
|
||||
local nz = e1x * e2y - e1y * e2x
|
||||
normals[a + 1] = normals[a + 1] + nx
|
||||
normals[a + 2] = normals[a + 2] + ny
|
||||
normals[a + 3] = normals[a + 3] + nz
|
||||
normals[b + 1] = normals[b + 1] + nx
|
||||
normals[b + 2] = normals[b + 2] + ny
|
||||
normals[b + 3] = normals[b + 3] + nz
|
||||
normals[c + 1] = normals[c + 1] + nx
|
||||
normals[c + 2] = normals[c + 2] + ny
|
||||
normals[c + 3] = normals[c + 3] + nz
|
||||
end
|
||||
for f = 0, 5 do
|
||||
local base = f * per
|
||||
for y = 0, n - 1 do
|
||||
for x = 0, n - 1 do
|
||||
local i = base + y * row + x
|
||||
tri(i, i + 1, i + n + 2)
|
||||
tri(i, i + n + 2, i + n + 1)
|
||||
end
|
||||
checkpoint()
|
||||
end
|
||||
end
|
||||
-- Faces duplicate their edge vertices; sum the copies so there are no lighting seams.
|
||||
local seams = {}
|
||||
for i = 0, count - 1 do
|
||||
local key = keys[i + 1]
|
||||
if key >= 0 then
|
||||
local g = seams[key]
|
||||
if g then
|
||||
g[#g + 1] = i
|
||||
else
|
||||
seams[key] = { i }
|
||||
end
|
||||
end
|
||||
if i % 4096 == 0 then
|
||||
checkpoint()
|
||||
end
|
||||
end
|
||||
checkpoint()
|
||||
for _, group in pairs(seams) do
|
||||
if #group > 1 then
|
||||
local sx, sy, sz = 0, 0, 0
|
||||
for _, i in ipairs(group) do
|
||||
sx, sy, sz = sx + normals[i * 3 + 1], sy + normals[i * 3 + 2], sz + normals[i * 3 + 3]
|
||||
end
|
||||
for _, i in ipairs(group) do
|
||||
normals[i * 3 + 1], normals[i * 3 + 2], normals[i * 3 + 3] = sx, sy, sz
|
||||
end
|
||||
end
|
||||
end
|
||||
for i = 0, count - 1 do
|
||||
local o = i * 3
|
||||
local nx, ny, nz = normals[o + 1], normals[o + 2], normals[o + 3]
|
||||
local len = sqrt(nx * nx + ny * ny + nz * nz)
|
||||
if len == 0 then
|
||||
len = 1
|
||||
end
|
||||
normals[o + 1], normals[o + 2], normals[o + 3] = nx / len, ny / len, nz / len
|
||||
if i % 2048 == 0 then
|
||||
checkpoint()
|
||||
end
|
||||
end
|
||||
checkpoint()
|
||||
|
||||
-- Colors: elevation gradient, dark patches, steep slopes, crater halos, grain.
|
||||
local sd = p.shading
|
||||
local low, mid, high = M.linear_color(sd.low), M.linear_color(sd.mid), M.linear_color(sd.high)
|
||||
local steep, patch = M.linear_color(sd.steep), M.linear_color(sd.patch)
|
||||
local colors = {}
|
||||
local range = max_h - min_h
|
||||
if range == 0 then
|
||||
range = 1
|
||||
end
|
||||
local patch_amount, patch_freq, grain = sd.patchAmount, sd.patchFrequency, sd.grain
|
||||
local st_lo, st_hi = sd.steepThreshold - sd.steepBlend, sd.steepThreshold + sd.steepBlend
|
||||
local cr_ejecta = cr.ejecta
|
||||
for i = 0, count - 1 do
|
||||
local o = i * 3
|
||||
local x, y, z = dirs[o + 1], dirs[o + 2], dirs[o + 3]
|
||||
local e = (heights[i + 1] - min_h) / range
|
||||
local lo, hi, te
|
||||
if e < 0.5 then
|
||||
lo, hi, te = low, mid, e * 2
|
||||
else
|
||||
lo, hi, te = mid, high, (e - 0.5) * 2
|
||||
end
|
||||
local c1 = lo[1] + (hi[1] - lo[1]) * te
|
||||
local c2 = lo[2] + (hi[2] - lo[2]) * te
|
||||
local c3 = lo[3] + (hi[3] - lo[3]) * te
|
||||
|
||||
if patch_amount > 0 then
|
||||
local pn = fbm(x + ox + 31.7, y + oy - 12.2, z + oz + 5.5, 4, patch_freq, 0.5, 2)
|
||||
-- Patches favor low ground, like maria.
|
||||
local w = smoothstep(-0.05, 0.4, pn) * patch_amount * (1 - e * 0.6)
|
||||
c1, c2, c3 = c1 + (patch[1] - c1) * w, c2 + (patch[2] - c2) * w, c3 + (patch[3] - c3) * w
|
||||
end
|
||||
|
||||
local g = ejecta[i + 1] * cr_ejecta
|
||||
c1, c2, c3 = c1 + (high[1] - c1) * g, c2 + (high[2] - c2) * g, c3 + (high[3] - c3) * g
|
||||
|
||||
local steepness = 1 - (normals[o + 1] * x + normals[o + 2] * y + normals[o + 3] * z)
|
||||
local ws = smoothstep(st_lo, st_hi, steepness)
|
||||
c1, c2, c3 = c1 + (steep[1] - c1) * ws, c2 + (steep[2] - c2) * ws, c3 + (steep[3] - c3) * ws
|
||||
|
||||
local gr = 1 + fbm(x + ox, y + oy, z + oz, 2, 14, 0.5, 2) * grain
|
||||
colors[o + 1], colors[o + 2], colors[o + 3] = c1 * gr, c2 * gr, c3 * gr
|
||||
if i % 128 == 0 then
|
||||
checkpoint()
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
n = n,
|
||||
count = count,
|
||||
positions = positions,
|
||||
normals = normals,
|
||||
colors = colors,
|
||||
min_h = min_h,
|
||||
max_h = max_h,
|
||||
}
|
||||
end
|
||||
|
||||
return M
|
||||
+15
-5
@@ -29,6 +29,7 @@
|
||||
-- p95, max); "reset" starts over
|
||||
|
||||
local focus = require("modules.focus")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -73,11 +74,21 @@ end
|
||||
-- Register an interactive element. kind: "button", "link", "tab", "field", "row", ...
|
||||
-- label: its text (for click "label=..."); zone: the stencil node clipping it, if any;
|
||||
-- no_focus: not in the keyboard focus order (the web's tabindex=-1).
|
||||
local function name(node, id)
|
||||
if id then
|
||||
names[gui.get_id(node)] = { id = id, scope = scope }
|
||||
end
|
||||
end
|
||||
|
||||
function M.register(id, node, kind, label, zone, no_focus)
|
||||
blocks.note(M.register, id, node, kind, label, zone, no_focus) -- (a kept block registers again)
|
||||
if zone and blocks.replaying() then
|
||||
zone = require("druid.helper").get_closest_stencil_node(node) -- (kept, perhaps under a new stencil)
|
||||
end
|
||||
if not no_focus then
|
||||
focus.add(id, node, kind, label, zone)
|
||||
end
|
||||
M.name(node, id)
|
||||
name(node, id)
|
||||
if not M.enabled or not id then
|
||||
return
|
||||
end
|
||||
@@ -97,9 +108,8 @@ end
|
||||
-- Give a node an element's id (register does; also a node that takes the clicks for a
|
||||
-- registered one). id_of: the id of a node, or nil.
|
||||
function M.name(node, id)
|
||||
if id then
|
||||
names[gui.get_id(node)] = { id = id, scope = scope }
|
||||
end
|
||||
blocks.note(name, node, id)
|
||||
name(node, id)
|
||||
end
|
||||
|
||||
function M.id_of(node)
|
||||
@@ -347,7 +357,7 @@ function COMMANDS.scroll_to(g, cmd)
|
||||
local scale = g.size[1] / sys.get_config_int("display.width", 1280)
|
||||
local off = (g.scroll_pos and g.scroll_pos[a.key] or 0) + (vy - ey) * scale
|
||||
g.scroll_pos[a.key] = math.max(0, math.min(a.max, off))
|
||||
g.dirty = true
|
||||
g.dirty, g.dirty_all = true, true
|
||||
return { ok = true, key = a.key, offset = g.scroll_pos[a.key] }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
local ui = require("modules.ui")
|
||||
local screen = require("modules.screen")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -59,6 +60,7 @@ function M.add(node, text)
|
||||
if not node or not text or text == "" then
|
||||
return
|
||||
end
|
||||
blocks.note(M.add, node, text) -- (a kept block adds it again)
|
||||
tips[#tips + 1] = { node = node, text = tostring(text), zone = zone_of(node) }
|
||||
end
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
-- difference at the rebuild is kept as an offset that eases to 0 while the value goes on
|
||||
-- ticking. Static values are the same thing with a constant function.
|
||||
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
||||
@@ -46,9 +48,14 @@ end
|
||||
-- source (the number itself for a static value, e.g. "start:end" for a timer): while it
|
||||
-- stays the same a running ease goes on; when it changes, the ease starts from what is
|
||||
-- shown. Returns the entry.
|
||||
local function seen(self, key)
|
||||
self.seen[key] = true
|
||||
end
|
||||
|
||||
function M:track(key, value, sig)
|
||||
sig = sig == nil and value or sig
|
||||
self.seen[key] = true
|
||||
seen(self, key)
|
||||
blocks.note(seen, self, key) -- (a kept block keeps the key)
|
||||
local e = self.vals[key]
|
||||
if not e then
|
||||
e = { shown = value, off = 0, t0 = 0, sig = sig }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// The viewport node: shows the moon scene the render script drew into `moon_tex` (HDR,
|
||||
// up to 2x the node's size: linear filtering at the pixel centers averages 2x2 samples) plus
|
||||
// its bloom (`bloom_tex`, half size, upsampled), with three.js's output pass: ACES
|
||||
// its bloom (`bloom_tex`, half the GUI reference size, upsampled), with three.js's output pass: ACES
|
||||
// filmic tone mapping (exposure) and the sRGB transfer.
|
||||
// Without a moon (moon_rect.z == 0) the node keeps its plain color.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#version 140
|
||||
|
||||
// The moon surface (vertex colors are linear RGB from terrain.lua).
|
||||
// The moon surface (vertex colors are linear RGB from mmterrain).
|
||||
const float MOON_RADIUS = 2.0; // scene.js MOON_RADIUS
|
||||
|
||||
in highp vec4 position;
|
||||
|
||||
+114
-64
@@ -1,5 +1,5 @@
|
||||
-- The moon scene (see modules/moon/moon.lua): builds meshes for the requested moon id
|
||||
-- (terrain.lua, spread over frames, cached per id), spins the moon, orbits the camera
|
||||
-- (the mmterrain extension; cached per id), spins the moon, orbits the camera
|
||||
-- (drag to rotate, wheel to zoom, like three.js OrbitControls with damping) and writes
|
||||
-- what the render script needs into moon.frame. Values follow server scene.js.
|
||||
--
|
||||
@@ -24,9 +24,6 @@ local FOV = math.rad(35)
|
||||
local RESUME = 1.5 -- seconds before auto-rotation resumes after a drag
|
||||
local ROTATE_SPEED = 0.6
|
||||
local DAMPING = 0.05
|
||||
local BUDGET = html5 and 0.008 or 0.006 -- seconds of generation work per frame (the web runs
|
||||
-- plain Lua, no LuaJIT: several times slower, so it gets more of each frame; 12 ms there
|
||||
-- overran 60 fps frames: 33 fps for the same ~4 s)
|
||||
local CACHE_SIZE = 6
|
||||
local SUPERSAMPLE = 2 -- the scene renders at 2x and is averaged down (antialiasing),
|
||||
-- counted in GUI reference pixels: a HiDPI display (or UI zoom) already gives the viewport
|
||||
@@ -185,44 +182,21 @@ local function mesh_key(id, settings)
|
||||
return id .. "|" .. terrain.serialize(rest)
|
||||
end
|
||||
|
||||
-- A job: a coroutine that generates the moon and fills a buffer, yielding when the
|
||||
-- frame's budget is spent.
|
||||
local WEB_RESOLUTION = 96 -- the web's cap on the grid (plain Lua: a quarter of the work of 180)
|
||||
-- A job generates one moon (the mmterrain extension, mmterrain/src/terrain.c) and fills
|
||||
-- its buffer: on a thread of its own on desktop; on the web (wasm, no threads) a share of
|
||||
-- each frame, then the buffer a part a frame.
|
||||
local native = mmterrain
|
||||
local NATIVE_SHARE, NATIVE_MIN, NATIVE_MAX = 0.3, 0.002, 0.012 -- (seconds of work a frame there)
|
||||
local NATIVE_FILL = 32768 -- strip vertices copied into the buffer a frame there
|
||||
local WEB_RESOLUTION = 128 -- the web's grid (desktop: the settings')
|
||||
|
||||
local function start_job(self, id, key, params)
|
||||
if html5 and params.resolution and params.resolution > WEB_RESOLUTION then
|
||||
params.resolution = WEB_RESOLUTION
|
||||
end
|
||||
local job = { id = id, key = key, params = params, started = now(), frames = 0, work = 0 }
|
||||
job.co = coroutine.create(function()
|
||||
local function checkpoint()
|
||||
if now() > job.deadline then
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
local m = terrain.generate(params, checkpoint)
|
||||
job.gen_work = job.work + (now() - job.resume_at)
|
||||
checkpoint()
|
||||
local strip = terrain.strip(m.n)
|
||||
checkpoint()
|
||||
local buf = buffer.create(#strip, STREAMS)
|
||||
local pos = buffer.get_stream(buf, hash("position"))
|
||||
local nor = buffer.get_stream(buf, hash("normal"))
|
||||
local col = buffer.get_stream(buf, hash("color"))
|
||||
local P, N, C = m.positions, m.normals, m.colors
|
||||
for k = 1, #strip do
|
||||
local v = strip[k] * 3
|
||||
local o = (k - 1) * 3
|
||||
pos[o + 1], pos[o + 2], pos[o + 3] = P[v + 1], P[v + 2], P[v + 3]
|
||||
nor[o + 1], nor[o + 2], nor[o + 3] = N[v + 1], N[v + 2], N[v + 3]
|
||||
col[o + 1], col[o + 2], col[o + 3] = C[v + 1], C[v + 2], C[v + 3]
|
||||
if k % 2048 == 0 then
|
||||
checkpoint()
|
||||
end
|
||||
end
|
||||
job.buffer = buf
|
||||
job.vertices = #strip
|
||||
end)
|
||||
local job = { id = id, key = key, params = params }
|
||||
job.gen = native.new(terrain.native_params(params))
|
||||
job.threaded = native.start(job.gen)
|
||||
self.job = job
|
||||
end
|
||||
|
||||
@@ -272,30 +246,88 @@ local function set_mesh(self, key, res, params)
|
||||
self.shown_params = params
|
||||
end
|
||||
|
||||
local function step_job(self)
|
||||
local job = self.job
|
||||
local t0 = now()
|
||||
job.resume_at = t0
|
||||
job.deadline = t0 + BUDGET
|
||||
job.frames = job.frames + 1
|
||||
local ok, err = coroutine.resume(job.co)
|
||||
job.work = job.work + (now() - t0)
|
||||
if not ok then
|
||||
print("moon: generation failed: " .. tostring(err))
|
||||
self.job = nil
|
||||
return
|
||||
end
|
||||
if coroutine.status(job.co) == "dead" then
|
||||
self.job = nil
|
||||
-- A finished job's buffer becomes a mesh resource in the cache (and the shown moon when
|
||||
-- it's the wanted one).
|
||||
local function job_done(self, job)
|
||||
local res = new_buffer_resource(job.buffer)
|
||||
cache_put(self, job.key, res)
|
||||
print(string.format("moon: %s (%s) built: %.0f ms of work (terrain %.0f, buffer %.0f) over %d frames (%.0f ms wall), %d vertices",
|
||||
job.id, job.params.technique, job.work * 1000, job.gen_work * 1000, (job.work - job.gen_work) * 1000,
|
||||
job.frames, (now() - job.started) * 1000, job.vertices))
|
||||
print(string.format("moon: %s (%s) built, %d vertices", job.id, job.params.technique, job.vertices))
|
||||
if job.key == self.want_key then
|
||||
set_mesh(self, job.key, res, job.params)
|
||||
end
|
||||
end
|
||||
|
||||
-- Done (threaded) or stepped to the end, then the buffer is filled (on the web a part a
|
||||
-- frame, the frames after the last step). Returns true when complete.
|
||||
local function step_native(self, job)
|
||||
if not job.buffer then
|
||||
local done
|
||||
if job.threaded then
|
||||
done = native.done(job.gen)
|
||||
else
|
||||
done = native.step(job.gen, math.max(NATIVE_MIN, math.min(NATIVE_MAX, (self.frame_dt or 0) * NATIVE_SHARE)))
|
||||
end
|
||||
if not done then
|
||||
return false
|
||||
end
|
||||
job.vertices = native.vertices(job.gen)
|
||||
job.buffer = buffer.create(job.vertices, STREAMS)
|
||||
job.filled = 0
|
||||
if not job.threaded then
|
||||
return false -- (the fill starts next frame)
|
||||
end
|
||||
end
|
||||
local to = job.threaded and job.vertices or math.min(job.vertices, job.filled + NATIVE_FILL)
|
||||
native.fill(job.gen, job.buffer, job.filled + 1, to)
|
||||
job.filled = to
|
||||
if to < job.vertices then
|
||||
return false
|
||||
end
|
||||
native.free(job.gen)
|
||||
job.gen = nil
|
||||
return true
|
||||
end
|
||||
|
||||
-- A job left behind for another moon: a thread runs to the end (freeing it would wait
|
||||
-- for it), and the moon goes into the cache; a stepped one is dropped.
|
||||
local function orphan(self, job)
|
||||
if job.threaded and type(job.gen) == "userdata" then
|
||||
self.orphans[#self.orphans + 1] = job
|
||||
elseif type(job.gen) == "userdata" then
|
||||
native.free(job.gen)
|
||||
end
|
||||
end
|
||||
|
||||
local function step_orphans(self)
|
||||
for i = #self.orphans, 1, -1 do
|
||||
local job = self.orphans[i]
|
||||
if native.done(job.gen) then
|
||||
table.remove(self.orphans, i)
|
||||
step_native(self, job)
|
||||
job_done(self, job)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function step_job(self)
|
||||
local job = self.job
|
||||
if step_native(self, job) then
|
||||
self.job = nil
|
||||
job_done(self, job)
|
||||
end
|
||||
end
|
||||
|
||||
-- The mesh for moon `id` (from the cache, or a job for it); returns its key and params.
|
||||
local function want(self, id)
|
||||
local params = terrain.resolve(moon.settings, id)
|
||||
local key = mesh_key(id, moon.settings)
|
||||
if not cache_get(self, key) and not (self.job and self.job.key == key) then
|
||||
if self.job then
|
||||
orphan(self, self.job)
|
||||
end
|
||||
start_job(self, id, key, params)
|
||||
end
|
||||
return key, params
|
||||
end
|
||||
|
||||
---------------------------------------------------------------- camera
|
||||
@@ -325,6 +357,7 @@ end
|
||||
function init(self)
|
||||
msg.post(".", "acquire_input_focus")
|
||||
self.cache = {}
|
||||
self.orphans = {}
|
||||
self.spin = 0
|
||||
self.spin_speed = 0
|
||||
self.dragging = false
|
||||
@@ -366,6 +399,12 @@ function final(self)
|
||||
for _, e in ipairs(self.cache) do
|
||||
resource.release(e.res)
|
||||
end
|
||||
if self.job and type(self.job.gen) == "userdata" then
|
||||
native.free(self.job.gen)
|
||||
end
|
||||
for _, job in ipairs(self.orphans) do
|
||||
native.free(job.gen)
|
||||
end
|
||||
resource.release(self.shell_res)
|
||||
resource.release(self.stars_res)
|
||||
resource.release(self.motes_res)
|
||||
@@ -373,18 +412,13 @@ end
|
||||
|
||||
function update(self, dt)
|
||||
dt = math.min(dt, 0.1)
|
||||
self.frame_dt = self.frame_dt and (self.frame_dt * 0.8 + dt * 0.2) or dt -- (smoothed, for the native budget)
|
||||
local f = moon.frame
|
||||
local rect = moon.rect
|
||||
local mystery = moon.mode == "mystery"
|
||||
if (not moon.id and not mystery) or not rect or rect.w <= 0 or rect.h <= 0 then
|
||||
f.visible = false
|
||||
return
|
||||
end
|
||||
if mystery and moon.settings and not self.mystery_values then
|
||||
self.mystery_values = render_values(terrain.resolve(moon.settings, "mystery"))
|
||||
end
|
||||
|
||||
-- The wanted mesh: from the cache, or start (or keep) a job for it.
|
||||
-- The wanted mesh: from the cache, or start (or keep) a job for it. A moon asked for
|
||||
-- ahead (moon.prefetch: a bought one, before its reveal shows) gets one too.
|
||||
if moon.settings and moon.id then
|
||||
if moon.id ~= self.want_id or moon.settings ~= self.want_settings then
|
||||
self.want_id = moon.id
|
||||
@@ -400,12 +434,28 @@ function update(self, dt)
|
||||
end
|
||||
end
|
||||
if self.shown_key ~= self.want_key and not (self.job and self.job.key == self.want_key) then
|
||||
start_job(self, moon.id, self.want_key, self.want_params)
|
||||
want(self, moon.id)
|
||||
end
|
||||
elseif moon.settings and moon.prefetch_id and not self.job then
|
||||
want(self, moon.prefetch_id)
|
||||
moon.prefetch_id = nil
|
||||
end
|
||||
if moon.prefetch_id == moon.id then
|
||||
moon.prefetch_id = nil
|
||||
end
|
||||
if self.job then
|
||||
step_job(self)
|
||||
end
|
||||
if #self.orphans > 0 then
|
||||
step_orphans(self)
|
||||
end
|
||||
if (not moon.id and not mystery) or not rect or rect.w <= 0 or rect.h <= 0 then
|
||||
f.visible = false
|
||||
return
|
||||
end
|
||||
if mystery and moon.settings and not self.mystery_values then
|
||||
self.mystery_values = render_values(terrain.resolve(moon.settings, "mystery"))
|
||||
end
|
||||
|
||||
local values = self.values
|
||||
if mystery then
|
||||
|
||||
@@ -57,7 +57,9 @@ local BLOOM_KERNELS = { 3, 5, 7, 9, 11 }
|
||||
local CLEAR_BLACK = { [graphics.BUFFER_TYPE_COLOR0_BIT] = vmath.vector4(0, 0, 0, 0) }
|
||||
|
||||
-- three.js UnrealBloomPass on the moon scene (moon_post material): high pass at half the
|
||||
-- viewport size, 5 mips of separable blur, composite into "bloom_h1".
|
||||
-- viewport's GUI reference size (the web's CSS pixels: a HiDPI display or UI zoom does the
|
||||
-- same work as 1x, and the blur's texels cover the same UI pixels, so the glow looks the
|
||||
-- same), 5 mips of separable blur, composite into "bloom_h1".
|
||||
local function draw_bloom(self, f, scene_rt)
|
||||
local p = self.predicates.moon_post
|
||||
local c = self.post_constants
|
||||
@@ -69,7 +71,8 @@ local function draw_bloom(self, f, scene_rt)
|
||||
render.clear(CLEAR_BLACK)
|
||||
render.draw(p, options)
|
||||
end
|
||||
local w, h = math.floor(f.rect.w / 2 + 0.5), math.floor(f.rect.h / 2 + 0.5)
|
||||
local u = math.max(1, moon.unit) -- (window pixels per GUI reference pixel)
|
||||
local w, h = math.max(1, math.floor(f.rect.w / u / 2 + 0.5)), math.max(1, math.floor(f.rect.h / u / 2 + 0.5))
|
||||
local bright = hdr_target(self, "bloom_bright", w, h)
|
||||
c.post = vmath.vector4(0, 0, 0, 0)
|
||||
c.post2 = vmath.vector4(0, 0, b.z, 0)
|
||||
|
||||
@@ -308,7 +308,10 @@ function M.build(self, root, px, py, w, h, H)
|
||||
end
|
||||
y = card.grid(self, c, x, y, inner, listed, function(cd, a)
|
||||
advisor_card(self, H, ad, cd, a)
|
||||
end)
|
||||
end, { key = function(a) return "adv:" .. tostring(a.name) end, sig = function()
|
||||
-- (kept while what it shows stays the same: modules/blocks)
|
||||
return { ad.selected or false, self.advisor_pick or false, self.inflight, self.view }
|
||||
end })
|
||||
area:finish(y + py)
|
||||
end
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
-- thinned to about one point per few pixels so a chart stays a few hundred nodes.
|
||||
|
||||
local ui = require("modules.ui")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -199,7 +200,7 @@ function M.chart(parent, x, y, w, h, data, opts)
|
||||
gui.set_position(tip, vec(tx, -math.max(0, math.min(ch - 22, cy - 30))))
|
||||
end
|
||||
if opts.pointers then
|
||||
opts.pointers[#opts.pointers + 1] = move
|
||||
blocks.push(opts.pointers, "pointers", move) -- (opts.pointers: the screen, g)
|
||||
end
|
||||
return box
|
||||
end
|
||||
|
||||
@@ -10,6 +10,7 @@ local button = require("components.button")
|
||||
local stat = require("components.stat")
|
||||
local anim = require("modules.anim")
|
||||
local test = require("modules.test")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local S = theme.size
|
||||
|
||||
@@ -141,7 +142,13 @@ function M.action(g, b, nodes, name, params, on_done, predictor, id)
|
||||
ready_look(g, b, key)
|
||||
end
|
||||
if g.autoplay then
|
||||
g.autoplay:register(name, type(params) == "function" and params() or params, fire)
|
||||
local p = type(params) == "function" and params() or params
|
||||
g.autoplay:register(name, p, fire)
|
||||
blocks.note(function() -- (a kept block registers again)
|
||||
if g.autoplay then
|
||||
g.autoplay:register(name, p, fire)
|
||||
end
|
||||
end)
|
||||
end
|
||||
return fire
|
||||
end
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
-- for the module contract the regions and sections follow.
|
||||
--
|
||||
-- Rebuild model: every new state (a response, a prediction) or a window/UI-scale change
|
||||
-- sets g.dirty and the next update rebuilds the whole layout from the state: the Druid
|
||||
-- instance and every layout node are dropped and made again. What must survive a rebuild
|
||||
-- lives on g (scroll offsets g.scroll_pos, fields g.fields, eased values g.tween, effects
|
||||
-- g.fx and toasts g.toasts on their own layers). Regions are laid out here: banner and
|
||||
-- header across the top, then the moon list, the main panel (the current section) and
|
||||
-- the aside.
|
||||
-- sets g.dirty and the next update rebuilds the layout from the state. The regions and the
|
||||
-- costly parts inside them are blocks (modules/blocks): one whose inputs (its signature)
|
||||
-- didn't change is kept with its nodes and Druid components, the rest is built again; a
|
||||
-- full rebuild (g.dirty_all: the theme, reduced motion, a size change, ...) drops the
|
||||
-- Druid instance and every layout node first. What must survive a rebuild lives on g
|
||||
-- (scroll offsets g.scroll_pos, fields g.fields, eased values g.tween, effects g.fx and
|
||||
-- toasts g.toasts on their own layers). Regions are laid out here: banner and header
|
||||
-- across the top, then the moon list, the main panel (the current section) and the aside.
|
||||
|
||||
local druid = require("druid.druid")
|
||||
local theme = require("modules.theme")
|
||||
@@ -36,6 +38,9 @@ local moonlist = require("screens.game.moonlist")
|
||||
local aside = require("screens.game.aside")
|
||||
local reveal = require("screens.game.reveal")
|
||||
local moon_view = require("modules.moon.moon")
|
||||
local blocks = require("modules.blocks")
|
||||
local sfx = require("modules.sfx")
|
||||
local ready = require("screens.game.core.ready")
|
||||
|
||||
local S = theme.size
|
||||
local NARROW_W = 1250 -- below this layout width the aside gets its own row (app.css)
|
||||
@@ -130,26 +135,6 @@ local function boot_screen(g, W, H)
|
||||
end
|
||||
end
|
||||
|
||||
local function build_main(g, parent, x, y, w, h)
|
||||
local st = g.state
|
||||
local p = panel.new(parent, x, y, w, h)
|
||||
g.main_panel = p
|
||||
local root = p.body
|
||||
local px, py = S.main_pad_x, S.main_pad_y
|
||||
local section = g.view.section
|
||||
local mod = M.SECTIONS[section]
|
||||
if g.reveal:hiding() then
|
||||
return -- what the new moon holds stays hidden until its card is closed
|
||||
elseif section == "new-moon" then
|
||||
local inner = math.min(w - px * 2, S.content_max)
|
||||
reveal.build_teaser(g, root, px + math.floor((w - px * 2 - inner) / 2), py, inner)
|
||||
elseif mod and (NO_BLOCK[section] or st[section]) then
|
||||
mod.build(g, root, px, py, w, h, g.h) -- modules center their own content
|
||||
elseif not mod then
|
||||
coming_soon(g, root, px, py, w)
|
||||
end
|
||||
end
|
||||
|
||||
-- Whether the aside shows, like the web's (page.html: hidden unless the view selects
|
||||
-- something for the viewport, views.go Scene.Select): a moon (s=moons with a moon), the
|
||||
-- new-moon teaser, the drill panel (Equipment with a drill) or an advisor's panel. Other
|
||||
@@ -193,12 +178,126 @@ function M.aside_shown(g)
|
||||
return false
|
||||
end
|
||||
|
||||
-- What each section shows from the state (besides the view and the screen-wide inputs):
|
||||
-- its block is kept while these stay the same (modules/blocks). The aside shows the
|
||||
-- section's own aside or the moon's well panel.
|
||||
local ALWAYS = { "rules", "unlocked", "catalogVersion", "bonuses", "liveEvents", "showNav" }
|
||||
local SECTION_KEYS = {
|
||||
moons = { "moon", "moons", "past", "tools", "well", "moonPrice", "stakes", "idle" },
|
||||
market = { "market" },
|
||||
refinery = { "refinery", "jobs", "refined", "credits", "resources" },
|
||||
leaderboard = { "leaderboard" },
|
||||
help = { "help" },
|
||||
equipment = { "equipment", "moons", "tools", "refined", "credits", "resources" },
|
||||
store = { "store", "credits", "moonPrice", "resources", "refined" },
|
||||
advisors = { "advisors", "credits" },
|
||||
["new-moon"] = { "newMoon", "moonPrice", "credits" },
|
||||
}
|
||||
local ASIDE_KEYS = { "moon", "moons", "past", "tools", "well", "stakes", "equipment", "advisors", "newMoon", "moonPrice", "credits", "refined" }
|
||||
|
||||
local function pick_keys(st, ...)
|
||||
local out = {}
|
||||
for _, list in ipairs({ ... }) do
|
||||
for _, k in ipairs(list) do
|
||||
if out[k] == nil then
|
||||
out[k] = st[k] == nil and false or st[k]
|
||||
end
|
||||
end
|
||||
end
|
||||
if st.catalogVersion == nil then
|
||||
out.catalog = st.catalog or false
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- Drills and batches whose end passed on our clock (core/ready counts them before the
|
||||
-- refetch): the counts they show change with it.
|
||||
local function ready_key(g)
|
||||
local st, now = g.state, session.now(g)
|
||||
local out = {}
|
||||
for _, list in ipairs({ st.stakes or {}, st.jobs or {} }) do
|
||||
for _, x in ipairs(list) do
|
||||
if not x.ready and x["end"] and x["end"] <= now then
|
||||
out[#out + 1] = tostring(x.id or x.toolId) .. "@" .. x["end"]
|
||||
end
|
||||
end
|
||||
end
|
||||
return table.concat(out, ",")
|
||||
end
|
||||
|
||||
-- What every region depends on besides the state: busy buttons, Claims that turned ready
|
||||
-- on our clock, the toggles.
|
||||
local function local_sig(g)
|
||||
return blocks.sig(g.inflight, g.ready_at, ready_key(g), sfx.muted(), anim.reduced, theme.name)
|
||||
end
|
||||
|
||||
-- The reveal hides the main panel, the aside's content and the new moon's row.
|
||||
local function reveal_sig(g)
|
||||
local r = g.reveal
|
||||
return blocks.sig(r:active(), r:hiding(), r.r and r.r.data and r.r.data.moonId or "")
|
||||
end
|
||||
|
||||
-- The section's block in the main panel.
|
||||
local function build_main(g, parent, x, y, w, h, common)
|
||||
local st = g.state
|
||||
local section = g.view.section
|
||||
local sig = blocks.sig(common, reveal_sig(g), x, y, w, h, g.view, g.advisor_pick, g.refine_qty, g.sale_open,
|
||||
pick_keys(st, ALWAYS, SECTION_KEYS[section] or {}))
|
||||
blocks.keep(g, "main", sig, parent, function(holder)
|
||||
for _, m in pairs(M.SECTIONS) do
|
||||
if m.teardown then
|
||||
m.teardown(g)
|
||||
end
|
||||
end
|
||||
g.well_nodes = {}
|
||||
g.frames = {} -- nested frames (cards) the main panel's entrance staggers
|
||||
local p = panel.new(holder, x, y, w, h)
|
||||
g.main_panel = p
|
||||
local root = p.body
|
||||
local px, py = S.main_pad_x, S.main_pad_y
|
||||
local mod = M.SECTIONS[section]
|
||||
if g.reveal:hiding() then
|
||||
return -- what the new moon holds stays hidden until its card is closed
|
||||
elseif section == "new-moon" then
|
||||
local inner = math.min(w - px * 2, S.content_max)
|
||||
reveal.build_teaser(g, root, px + math.floor((w - px * 2 - inner) / 2), py, inner)
|
||||
elseif mod and (NO_BLOCK[section] or st[section]) then
|
||||
mod.build(g, root, px, py, w, h, g.h) -- modules center their own content
|
||||
elseif not mod then
|
||||
coming_soon(g, root, px, py, w)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- The aside's block (narrow: the row under the panels).
|
||||
local function build_aside(g, parent, x, y, w, h, row, common)
|
||||
local st = g.state
|
||||
local section = g.view.section
|
||||
local sig = blocks.sig(common, reveal_sig(g), x, y, w, h, row, g.view, g.advisor_pick, g.sale_open,
|
||||
pick_keys(st, ALWAYS, SECTION_KEYS[section] or {}, ASIDE_KEYS))
|
||||
return blocks.keep(g, "aside", sig, parent, function(holder)
|
||||
return aside.build(g, holder, x, y, w, h, g.h, M.SECTIONS[section], row)
|
||||
end)
|
||||
end
|
||||
|
||||
-- A full rebuild drops every node and component and builds them all again; otherwise the
|
||||
-- regions (and the blocks inside them) whose inputs didn't change are kept (modules/
|
||||
-- blocks). Full: the first state, a window or UI-scale change, the narrow layout (one
|
||||
-- page scroll), the theme or another screen-wide toggle (g.dirty_all).
|
||||
function M.build(g)
|
||||
g.builds = (g.builds or 0) + 1 -- (test counter)
|
||||
local W, H = screen.fit(g.root)
|
||||
local size = { W, H, select(3, screen.layout()) }
|
||||
local narrow = W < NARROW_W
|
||||
local full = not g.layout or not g.state or not g.layout_state or g.dirty_all or narrow
|
||||
or not g.size or g.size[1] ~= size[1] or g.size[2] ~= size[2] or g.size[3] ~= size[3]
|
||||
g.dirty_all = false
|
||||
g.full_builds = (g.full_builds or 0) + (full and 1 or 0) -- (test counter)
|
||||
-- The page's ground and its top-right glow (game.gui) in the theme's colors.
|
||||
gui.set_color(gui.get_node("bg"), theme.color.bg)
|
||||
gui.set_color(gui.get_node("glow"), theme.color.bg_glow)
|
||||
button.begin_build() -- (a press the rebuild interrupts carries over: components/button)
|
||||
if full then
|
||||
each_module(function(m)
|
||||
if m.teardown then
|
||||
m.teardown(g)
|
||||
@@ -209,41 +308,57 @@ function M.build(g)
|
||||
g.druid:final()
|
||||
g.tearing_down = false
|
||||
g.druid = druid.new(g)
|
||||
blocks.hook(g.druid)
|
||||
gui.delete_node(g.layout)
|
||||
g.layout = nil
|
||||
end
|
||||
local W, H = screen.fit(g.root)
|
||||
g.size = { W, H, select(3, screen.layout()) }
|
||||
blocks.clear(g)
|
||||
end
|
||||
blocks.begin(g, full)
|
||||
g.size = size
|
||||
gui.set_size(g.page, vec(W, H))
|
||||
gui.set_position(g.page, vec(-W / 2, H / 2))
|
||||
g.tickers = {}
|
||||
g.scroll_areas = {}
|
||||
g.tween:begin()
|
||||
g.tab_nodes, g.moon_nodes, g.well_nodes = {}, {}, {}
|
||||
g.fx:begin_build()
|
||||
test.begin_build()
|
||||
tooltip.begin_build()
|
||||
g.inputs = {} -- text fields (core/keys: Enter submits, keys skip while editing)
|
||||
g.pointers = {} -- mouse-move handlers (the market chart's crosshair)
|
||||
if g.autoplay then
|
||||
g.autoplay:reset()
|
||||
end
|
||||
|
||||
if not g.layout then
|
||||
g.layout = ui.rect(g.page, 0, 0, W, H, "none")
|
||||
gui.move_below(g.layout, g.toast_layer)
|
||||
end
|
||||
g.toasts:set_height(H)
|
||||
g.dialog:layout(W, H)
|
||||
if not g.state then
|
||||
g.tab_nodes, g.moon_nodes, g.well_nodes = {}, {}, {}
|
||||
boot_screen(g, W, H)
|
||||
g.layout_state = false
|
||||
g.dirty = false
|
||||
blocks.finish(g)
|
||||
return
|
||||
end
|
||||
g.pointers = {} -- mouse-move handlers (the market chart's crosshair)
|
||||
-- Announcements and live events above the header.
|
||||
local banner_h = banner.build(g, g.h, g.layout, W)
|
||||
local header_root = ui.rect(g.layout, 0, banner_h, W, 1, "none")
|
||||
g.header_node = header_root
|
||||
local header_h = header.build(g, header_root, 0, 0, W, nil, g.h) + banner_h
|
||||
|
||||
g.layout_state = true
|
||||
local st = g.state
|
||||
local common = local_sig(g)
|
||||
-- Announcements and live events above the header.
|
||||
local banner_h = blocks.keep(g, "banner", blocks.sig(common, W, st.banner), g.layout, function(holder)
|
||||
return banner.build(g, g.h, holder, W)
|
||||
end)
|
||||
local header_h = blocks.keep(g, "header", blocks.sig(common, W, banner_h, g.view.section, st.credits, st.resources,
|
||||
st.unlocked, st.shine, st.ready, st.nextEnd, ready.drills(g), ready.batches(g)), g.layout, function(holder)
|
||||
g.tab_nodes = {}
|
||||
local header_root = ui.rect(holder, 0, banner_h, W, 1, "none")
|
||||
g.header_node = header_root
|
||||
return header.build(g, header_root, 0, 0, W, nil, g.h) + banner_h
|
||||
end)
|
||||
|
||||
local pad, gap = S.body_pad, S.gap
|
||||
local top = header_h + pad
|
||||
local body_h = H - top - pad
|
||||
@@ -251,7 +366,6 @@ function M.build(g)
|
||||
-- Narrow windows (the web's @media max-width 1250px): the aside goes to its own row
|
||||
-- under the moon list and the main panel, the viewport and the side panel side by
|
||||
-- side, and the page scrolls to it (a page scroll area under the header).
|
||||
local narrow = W < NARROW_W
|
||||
local aside_w = has_aside and math.max(S.aside_w_min, math.min(S.aside_w_max, math.floor(W * 0.33))) or 0
|
||||
-- The moon list shows once there is a moon (or the store is open), like the web.
|
||||
local list_w = math.max(S.moonlist_w, math.min(S.moonlist_w_max, math.floor(W * 0.15)))
|
||||
@@ -279,12 +393,24 @@ function M.build(g)
|
||||
parent, y0 = page.content, pad
|
||||
end
|
||||
if list_w > 0 then
|
||||
moonlist.build(g, g.h, parent, pad, y0, list_w, body_h)
|
||||
-- (the rows' ready counts: per moon, from the stakes on our clock)
|
||||
local v = g.view
|
||||
local hides = {}
|
||||
for _, m in ipairs(st.moons) do
|
||||
hides[#hides + 1] = g.reveal:hides(m.id) or false
|
||||
end
|
||||
g.frames = {} -- nested frames (cards) the main panel's entrance staggers
|
||||
build_main(g, parent, main_x, y0, main_w, body_h)
|
||||
blocks.keep(g, "moonlist", blocks.sig(common, reveal_sig(g), pad, y0, list_w, body_h, v.section, v.past, v.page, v.moon,
|
||||
st.moons, st.past, st.pastPage, st.unlocked, st.moonPrice, hides), parent, function(holder)
|
||||
g.moon_nodes = {}
|
||||
moonlist.teardown(g)
|
||||
moonlist.build(g, g.h, holder, pad, y0, list_w, body_h)
|
||||
end)
|
||||
else
|
||||
g.moon_nodes = {}
|
||||
end
|
||||
build_main(g, parent, main_x, y0, main_w, body_h, common)
|
||||
if page then
|
||||
g.aside_panel = aside.build(g, parent, pad, y0 + body_h + gap, aside_w, row_h, g.h, M.SECTIONS[g.view.section], true)
|
||||
g.aside_panel = build_aside(g, parent, pad, y0 + body_h + gap, aside_w, row_h, true, common)
|
||||
local s = page:finish(y0 + body_h + gap + row_h + pad)
|
||||
if g.viewport_node then
|
||||
moon_view.move(g.viewport_node) -- (where the page's offset put it)
|
||||
@@ -311,7 +437,7 @@ function M.build(g)
|
||||
end)
|
||||
end
|
||||
elseif has_aside then
|
||||
g.aside_panel = aside.build(g, parent, W - pad - aside_w, top, aside_w, body_h, g.h, M.SECTIONS[g.view.section])
|
||||
g.aside_panel = build_aside(g, parent, W - pad - aside_w, top, aside_w, body_h, nil, common)
|
||||
else
|
||||
g.aside_panel, g.viewport_node, g.viewport_rect, g.view_was_mystery = nil, nil, nil, false
|
||||
moon_view.hide()
|
||||
@@ -319,6 +445,8 @@ function M.build(g)
|
||||
if not page then
|
||||
g.page_key, g.page_main = nil, nil
|
||||
end
|
||||
blocks.finish(g)
|
||||
ready.watch(g) -- (rebuild when the next drill or batch end passes)
|
||||
enter(g)
|
||||
g.tween:finish()
|
||||
ticks.run(g)
|
||||
@@ -370,6 +498,7 @@ function M.init(g)
|
||||
end
|
||||
msg.post(".", "acquire_input_focus")
|
||||
g.druid = druid.new(g)
|
||||
blocks.hook(g.druid)
|
||||
g.root = gui.get_node("root")
|
||||
g.page = gui.get_node("page")
|
||||
theme.set(theme.name) -- the saved theme's clear color (the render script starts dark)
|
||||
@@ -403,6 +532,7 @@ function M.final(g)
|
||||
end)
|
||||
g.dialog:remove()
|
||||
g.druid:final()
|
||||
blocks.clear(g)
|
||||
end
|
||||
|
||||
local TOUCH = hash("touch")
|
||||
@@ -460,7 +590,7 @@ local TEST_API = {
|
||||
g.scroll_pos = g.scroll_pos or {}
|
||||
g.scroll_pos[k:sub(8)] = tonumber(v) or 0
|
||||
end
|
||||
g.dirty = true
|
||||
g.dirty, g.dirty_all = true, true
|
||||
end,
|
||||
}
|
||||
|
||||
@@ -468,7 +598,10 @@ function M.update(g, dt)
|
||||
local t0 = test.enabled and test.clock()
|
||||
local built
|
||||
local W, H, scale = screen.layout()
|
||||
if g.dirty or W ~= g.size[1] or H ~= g.size[2] or scale ~= g.size[3] then
|
||||
local resized = W ~= g.size[1] or H ~= g.size[2] or scale ~= g.size[3]
|
||||
-- (while the reveal card flies into the moon list, a new state waits for it to land:
|
||||
-- the page is hidden under it anyway, and the flight keeps its frames)
|
||||
if resized or (g.dirty and not g.reveal:closing()) then
|
||||
M.build(g)
|
||||
built = t0 and test.clock() - t0
|
||||
end
|
||||
@@ -534,7 +667,7 @@ function M.on_reload(g)
|
||||
theme.set(theme.name) -- (the page's clear color)
|
||||
ensure(g)
|
||||
g.h = hooks.table()
|
||||
g.dirty = true
|
||||
g.dirty, g.dirty_all = true, true
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
local ui = require("modules.ui")
|
||||
local session = require("screens.game.core.session")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -20,7 +21,7 @@ function M.tick_text(g, node, ends, fmt, done, key, on_done)
|
||||
if key then
|
||||
g.tween:track(key, ends - now(g), ends)
|
||||
end
|
||||
g.tickers[#g.tickers + 1] = { kind = "time", node = node, ends = ends, fmt = fmt, done = done, key = key, on_done = on_done }
|
||||
blocks.push(g, "tickers", { kind = "time", node = node, ends = ends, fmt = fmt, done = done, key = key, on_done = on_done })
|
||||
end
|
||||
|
||||
local function bar_fill(t, start, ends)
|
||||
@@ -33,7 +34,7 @@ function M.tick_bar(g, node, start, ends, w, key, on_full)
|
||||
if key then
|
||||
g.tween:track(key, bar_fill(now(g), start, ends), start .. ":" .. ends)
|
||||
end
|
||||
g.tickers[#g.tickers + 1] = { kind = "bar", node = node, start = start, ends = ends, w = w, key = key, on_done = on_full }
|
||||
blocks.push(g, "tickers", { kind = "bar", node = node, start = start, ends = ends, w = w, key = key, on_done = on_full })
|
||||
end
|
||||
|
||||
-- A number text node showing fmt(value), easing from the shown value when it changes.
|
||||
@@ -41,7 +42,7 @@ end
|
||||
function M.tick_num(g, node, key, value, fmt)
|
||||
g.tween:track(key, value)
|
||||
local k = { kind = "num", node = node, key = key, value = value, fmt = fmt or ui.money }
|
||||
g.tickers[#g.tickers + 1] = k
|
||||
blocks.push(g, "tickers", k)
|
||||
local v = g.tween:value(key, value)
|
||||
if v ~= value and value == math.floor(value) then
|
||||
v = math.floor(v + 0.5) -- counts tick as whole numbers
|
||||
@@ -55,7 +56,7 @@ function M.run(g)
|
||||
local t = now(g)
|
||||
local tw = g.tween
|
||||
for _, k in ipairs(g.tickers) do
|
||||
if k.dead then
|
||||
if k.dead or not pcall(gui.get_id, k.node) then
|
||||
-- node gone (deleted outside a rebuild)
|
||||
elseif k.kind == "time" then
|
||||
local left = k.ends - t
|
||||
|
||||
@@ -225,7 +225,11 @@ function M.build(self, root, px, py, w, h, H)
|
||||
end
|
||||
y = card.grid(self, c, x, y, inner, eq.tools, function(cd, t)
|
||||
tool_card(self, H, st, cd, t, open)
|
||||
end)
|
||||
end, { key = function(t) return "eq:tool:" .. tostring(t.id) end, sig = function(t)
|
||||
-- (kept while what it shows stays the same: modules/blocks)
|
||||
local moon = t.stake and find(st.moons, "id", t.stake.moonId)
|
||||
return { open, moon and moon.name or false, st.refined, st.credits, st.resources, st.catalogVersion, self.inflight, self.view }
|
||||
end })
|
||||
area:finish(y + py)
|
||||
end
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ local function wallet(g, parent, W)
|
||||
tooltip.add(mute, sfx.muted() and "Sound off" or "Sound on")
|
||||
g.druid:new_button(mute, function()
|
||||
sfx.toggle_mute()
|
||||
g.dirty = true
|
||||
g.dirty, g.dirty_all = true, true
|
||||
end):set_animations_disabled()
|
||||
-- Light / dark theme (saved); the icon shows the theme it switches to, like the web.
|
||||
local tx = cx - 14 - 28 - 8
|
||||
@@ -88,7 +88,7 @@ local function wallet(g, parent, W)
|
||||
tooltip.add(tgl, "Light / dark theme")
|
||||
g.druid:new_button(tgl, function()
|
||||
theme.toggle()
|
||||
g.dirty = true
|
||||
g.dirty, g.dirty_all = true, true
|
||||
end):set_animations_disabled()
|
||||
-- Reduced motion (saved): speed lines, lit while motion is on (the web follows the
|
||||
-- system's setting; here it's the player's).
|
||||
@@ -109,7 +109,7 @@ local function wallet(g, parent, W)
|
||||
g.druid:new_button(mot, function()
|
||||
anim.set_reduced_motion(not anim.reduced)
|
||||
sfx.play("toggle")
|
||||
g.dirty = true
|
||||
g.dirty, g.dirty_all = true, true
|
||||
end):set_animations_disabled()
|
||||
-- Display settings (UI scale, fullscreen: screens/game/settings.lua); a screen icon.
|
||||
local dx = mx - 28 - 8
|
||||
|
||||
@@ -15,6 +15,7 @@ local chart = require("screens.game.chart")
|
||||
local ticker = require("screens.game.ticker")
|
||||
local sale = require("screens.game.sale")
|
||||
local scroll = require("components.scroll")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
-- Windows this narrow (layout units) stack the detail and the sell ticket, and the
|
||||
-- section scrolls (app.css @media max-width 1100px: .mk-grid one column).
|
||||
@@ -168,7 +169,11 @@ local function card(self, H, root, x, y, w, h, q, on)
|
||||
ui.text(c, boost_text(q), { font = "mono", px = 11, color = "ready" }, w - 10, cy, gui.PIVOT_E)
|
||||
end
|
||||
end
|
||||
chart.spark(c, 10, h - 20, w - 20, 13, q.spark, (q.change >= 0) and "up" or "danger")
|
||||
-- (kept while the series is the same: it moves on with the 5-minute tick)
|
||||
local spark_color = (q.change >= 0) and "up" or "danger"
|
||||
blocks.keep(self, "mk:spark:" .. q.item, blocks.sig(w, h, q.spark, spark_color), c, function(r)
|
||||
chart.spark(r, 10, h - 20, w - 20, 13, q.spark, spark_color)
|
||||
end)
|
||||
-- Hover (app.css .mk-card:hover): a brighter edge and fill; the selected card keeps its look.
|
||||
local b = H.link(self, c, quote_view(self, q.item))
|
||||
if not on then
|
||||
@@ -224,8 +229,14 @@ function M.build(self, root, px, py, w, h, H, opts)
|
||||
|
||||
-- Ticker tape across the panel.
|
||||
local cy = 14
|
||||
cy = cy + ticker.build(self, root, 14, cy, w - 28, mk.quotes, function(item) return color_of(st, item) end, function(item)
|
||||
local tape = {}
|
||||
for i, q in ipairs(mk.quotes) do
|
||||
tape[i] = { q.item, q.symbol, q.price, q.up, q.change, q.changeText }
|
||||
end
|
||||
cy = cy + blocks.keep(self, "mk:ticker", blocks.sig(cy, w, tape), root, function(r)
|
||||
return ticker.build(self, r, 14, cy, w - 28, mk.quotes, function(item) return color_of(st, item) end, function(item)
|
||||
H.navigate(self, quote_view(self, item))
|
||||
end)
|
||||
end) + 8
|
||||
|
||||
-- Heading, with the holdings stats on its right.
|
||||
@@ -401,9 +412,13 @@ function M.build(self, root, px, py, w, h, H, opts)
|
||||
end
|
||||
ui.rect(d, 14, ohl_y - 8, dw - 28, 1, "line")
|
||||
if mk.chart and mk.chart.item == q.item then
|
||||
chart.chart(d, 14, y + 4, dw - 28, ohl_y - 16 - (y + 4), mk.chart, {
|
||||
color = up and "up" or "danger", events = price_events(st, q.item), pointers = self.pointers,
|
||||
local events = price_events(st, q.item)
|
||||
local ch = ohl_y - 16 - (y + 4)
|
||||
blocks.keep(self, "mk:chart", blocks.sig(y, dw, ch, up, mk.chart, events), d, function(r)
|
||||
chart.chart(r, 14, y + 4, dw - 28, ch, mk.chart, {
|
||||
color = up and "up" or "danger", events = events, pointers = self,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
-- Sell ticket.
|
||||
@@ -423,7 +438,8 @@ function M.build(self, root, px, py, w, h, H, opts)
|
||||
-- Payout: what the amount typed would earn, or Sell all with none (display only).
|
||||
-- After a sale: what it earned, powering down (sale.lua).
|
||||
local pay, pay_tick, sale_n
|
||||
local function payout()
|
||||
local payout
|
||||
payout = function()
|
||||
if pay_tick then
|
||||
pay_tick.dead = true
|
||||
pay_tick = nil
|
||||
@@ -476,6 +492,7 @@ function M.build(self, root, px, py, w, h, H, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
payout = blocks.bound(payout) -- (made again by the field and the sale: the market's block)
|
||||
local sell_now -- (the Sell button's click: Enter in the field sells, like the web's form)
|
||||
local input = field.new(self, t, "sell", 14, 52, tin, 36, {
|
||||
hint = "0", font = "mono", hint_font = "mono", allowed = "[%d%.]",
|
||||
|
||||
@@ -9,6 +9,7 @@ local theme = require("modules.theme")
|
||||
local ui = require("modules.ui")
|
||||
local tooltip = require("modules.tooltip")
|
||||
local test = require("modules.test")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -57,12 +58,18 @@ function M.build(self, H, root, x, y, w, moon)
|
||||
end
|
||||
if sale.canSell then
|
||||
cards.confirm_server_button(self, b, sale.confirm, "sell-moon", { ["sell-moon"] = moon.id }, "sell-moon")
|
||||
if self.autoplay then
|
||||
local function reg()
|
||||
if self.autoplay then
|
||||
self.autoplay:register("sell-moon", { ["sell-moon"] = moon.id }, function()
|
||||
H.act(self, "sell-moon", { ["sell-moon"] = moon.id })
|
||||
end)
|
||||
end
|
||||
end
|
||||
reg()
|
||||
blocks.note(reg) -- (a kept block registers again)
|
||||
end
|
||||
end
|
||||
self.druid:new_button(hit, function()
|
||||
self.sale_open[moon.id] = not open
|
||||
self.dirty = true
|
||||
|
||||
@@ -20,6 +20,7 @@ local ready = require("screens.game.core.ready")
|
||||
local tabs = require("components.tabs")
|
||||
local list_c = require("components.list")
|
||||
local row_c = require("components.row")
|
||||
local blocks = require("modules.blocks")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -401,9 +402,9 @@ function M.build(self, H, parent, x, y, w, h)
|
||||
local rows_y = cy + 34 + 12
|
||||
field.new(self, root, "find", pad, cy, inner, 34, {
|
||||
hint = "Name", px = 14, max_len = 40, esc_clears = true, -- (type=search)
|
||||
on_change = function(text)
|
||||
on_change = blocks.bound(function(text) -- (what it makes is the moon list's: modules/blocks)
|
||||
build_rows(text, rows_y)
|
||||
end,
|
||||
end),
|
||||
})
|
||||
cy = rows_y
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
local theme = require("modules.theme")
|
||||
local ui = require("modules.ui")
|
||||
local blocks = require("modules.blocks")
|
||||
local tooltip = require("modules.tooltip")
|
||||
local test = require("modules.test")
|
||||
local scroll = require("components.scroll")
|
||||
@@ -221,8 +222,7 @@ local function recipe_row(self, root, H, st, rf, r, x, y, w)
|
||||
H.action(self, mx, mn, "refine", { recipe = r.item, all = "1" })
|
||||
tooltip.add(mx, "Refine as many as you can (" .. ui.money(r.maxQty) .. ")")
|
||||
-- Enter in the quantity refines (the web's form).
|
||||
self.inputs = self.inputs or {}
|
||||
self.inputs[#self.inputs + 1] = { input = input, submit = refine, step = { min = 1, max = rf.maxBatch } }
|
||||
blocks.push(self, "inputs", { input = input, submit = refine, step = { min = 1, max = rf.maxBatch } })
|
||||
else
|
||||
tooltip.add(rb, why)
|
||||
tooltip.add(mx, why)
|
||||
|
||||
@@ -198,6 +198,11 @@ function M:hiding()
|
||||
return self.r ~= nil
|
||||
end
|
||||
|
||||
-- The card is flying away (Start mining).
|
||||
function M:closing()
|
||||
return self.r ~= nil and self.r.closing ~= nil
|
||||
end
|
||||
|
||||
-- The new moon's entry in the moon list stays hidden too.
|
||||
function M:hides(id)
|
||||
return self.r ~= nil and self.r.data.moonId == id
|
||||
@@ -307,6 +312,7 @@ function M:start(data, toast)
|
||||
-- Reduced motion (reveal.js quick): no charge-up, no particles or shake, the card fades
|
||||
-- in complete, silently; the moon only fades out of its shroud (moon.script).
|
||||
r.reduced = anim.reduced
|
||||
moon_view.prefetch(data.moonId) -- (its mesh starts now, whether the viewport shows yet or not)
|
||||
if g.viewport_node then
|
||||
moon_view.reveal(data.moonId, g.viewport_node)
|
||||
if moon_view.revealing then
|
||||
@@ -367,9 +373,18 @@ function M:place()
|
||||
r.v = v
|
||||
end
|
||||
|
||||
-- Druid components for the layer: once per Druid instance (the game screen's Druid is
|
||||
-- recreated on every build), the card's once it exists. Registered after the layout's,
|
||||
-- so they take the input first.
|
||||
-- Druid components for the layer: once per Druid instance (the game screen's lives until
|
||||
-- a full rebuild), the card's once it exists. They take the input before the page's
|
||||
-- (a high input priority: page blocks rebuilt later are newer).
|
||||
local PRIORITY = 20 -- (Druid's PRIORITY_INPUT_HIGH; the page's are 10)
|
||||
|
||||
local function first(c)
|
||||
if c and c.set_input_priority then
|
||||
c:set_input_priority(PRIORITY)
|
||||
end
|
||||
return c
|
||||
end
|
||||
|
||||
function M:bind()
|
||||
local r = self.r
|
||||
if not r then
|
||||
@@ -379,38 +394,42 @@ function M:bind()
|
||||
if self.bound ~= g.druid then
|
||||
self.bound, self.card_bound = g.druid, nil
|
||||
self.components = {}
|
||||
local hit = g.druid:new_button(r.hit, function()
|
||||
local hit = first(g.druid:new_button(r.hit, function()
|
||||
self:advance()
|
||||
end):set_animations_disabled()
|
||||
end):set_animations_disabled())
|
||||
self.components[#self.components + 1] = hit
|
||||
g.druid:new_hotkey("key_space", function() self:advance() end)
|
||||
g.druid:new_hotkey("key_enter", function() self:advance() end)
|
||||
g.druid:new_hotkey("key_esc", function()
|
||||
self.components[#self.components + 1] = first(g.druid:new_hotkey("key_space", function() self:advance() end))
|
||||
self.components[#self.components + 1] = first(g.druid:new_hotkey("key_enter", function() self:advance() end))
|
||||
self.components[#self.components + 1] = first(g.druid:new_hotkey("key_esc", function()
|
||||
if self.r and self.r.card then
|
||||
self:close()
|
||||
else
|
||||
self:advance()
|
||||
end
|
||||
end)
|
||||
end))
|
||||
end
|
||||
if r.card and self.card_bound ~= g.druid then
|
||||
self.card_bound = g.druid
|
||||
self.components[#self.components + 1] = g.druid:new_button(r.close_btn, function()
|
||||
self.components[#self.components + 1] = first(g.druid:new_button(r.close_btn, function()
|
||||
if r.done then
|
||||
self:close()
|
||||
else
|
||||
self:advance()
|
||||
end
|
||||
end)
|
||||
end))
|
||||
if r.another_btn and r.another_ok then
|
||||
self.components[#self.components + 1] = first(g.druid:new_button(r.another_btn, function()
|
||||
self:buy_another()
|
||||
end))
|
||||
end
|
||||
end
|
||||
-- (every build resets these)
|
||||
if r.card then
|
||||
if g.autoplay then
|
||||
g.autoplay:register("reveal-close", {}, function() self:close() end)
|
||||
end
|
||||
if r.another_btn and r.another_ok then
|
||||
self.components[#self.components + 1] = g.druid:new_button(r.another_btn, function()
|
||||
self:buy_another()
|
||||
end)
|
||||
elseif r.another_btn then
|
||||
tooltip.add(r.another_btn, r.another_why) -- (tooltips go with every page build)
|
||||
if r.another_btn and not r.another_ok then
|
||||
tooltip.add(r.another_btn, r.another_why)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -168,16 +168,20 @@ function M.build(self, root, px, py, w, h, H)
|
||||
|
||||
ui.text(c, "Tools", "cap", x, y + 6)
|
||||
y = y + 20
|
||||
-- (cards are kept while what they show stays the same: modules/blocks)
|
||||
local function shared()
|
||||
return { st.credits, st.moonPrice, st.catalogVersion, self.inflight, self.view }
|
||||
end
|
||||
y = card.grid(self, c, x, y, inner, sto.tools, function(cd, t)
|
||||
tool_card(self, H, st, cd, t)
|
||||
end)
|
||||
end, { key = function(t) return "store:tool:" .. tostring(t.storeItemId) end, sig = shared })
|
||||
if #(sto.attachments or {}) > 0 then
|
||||
y = y + 8
|
||||
ui.text(c, "Attachments", "cap", x, y + 6)
|
||||
y = y + 20
|
||||
y = card.grid(self, c, x, y, inner, sto.attachments, function(cd, a)
|
||||
gear_card(self, H, st, cd, a)
|
||||
end)
|
||||
end, { key = function(a) return "store:gear:" .. tostring(a.name) end, sig = shared })
|
||||
end
|
||||
area:finish(y + py)
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- The Market's ticker tape (the web's .mk-tape): every quote's symbol, price and 24h
|
||||
-- change, scrolling left in a clipped strip; a click picks the quote. The strip is
|
||||
-- rebuilt with every state, so its phase comes from a clock kept on the screen
|
||||
-- rebuilt when its quotes change (a block), so its phase comes from a clock kept on the screen
|
||||
-- (g.tape_t0): a refresh doesn't jump it back to the start. It pauses under the mouse
|
||||
-- (app.css .mk-tape:hover): the clock shifts by the time it held. Still with reduced motion.
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
- characters the fonts lack: non-ASCII characters in Lua string literals and in a
|
||||
sample of server texts (GET /api/state of a fixture player, every section) must be in
|
||||
the fonts that draw them (text fonts; the symbols ui.rich sends to the sym font)
|
||||
- the moon generator (scripts/moon-check.mjs: the mmterrain extension's C, built for the
|
||||
host, and terrain.lua's settings side, against the server's terrain.js)
|
||||
- engine warnings in a scenario run's log: font batches / characters / text entries
|
||||
over their limits, out of nodes or animations, stencil overflow, invalid markup,
|
||||
script errors
|
||||
@@ -226,10 +228,20 @@ def check_scenario(api):
|
||||
check_log(os.path.join(ROOT, "dist/drive/check/game.log"))
|
||||
|
||||
|
||||
def check_moon():
|
||||
r = subprocess.run(["node", os.path.join(HERE, "moon-check.mjs")], cwd=ROOT, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, text=True)
|
||||
if r.returncode != 0:
|
||||
fail("moon-check.mjs failed:\n" + "\n".join(l for l in r.stdout.splitlines() if not l.startswith("ok "))[-2000:])
|
||||
else:
|
||||
print("moon: " + (r.stdout.strip().splitlines() or [""])[-1])
|
||||
|
||||
|
||||
def main():
|
||||
api = os.environ.get("MM_API", "http://localhost:3000").rstrip("/")
|
||||
check_lua()
|
||||
check_flags()
|
||||
check_moon()
|
||||
check_fonts(api)
|
||||
if "--static" not in sys.argv:
|
||||
check_scenario(api)
|
||||
|
||||
+103
-22
@@ -1,11 +1,16 @@
|
||||
// Checks the Lua moon generator (modules/moon/terrain.lua) against the server's
|
||||
// terrain.js: same random draws, traits and colors, and the same mesh within float32
|
||||
// rounding. Needs node and luajit.
|
||||
// Checks the moon generator against the server's terrain.js: the native one
|
||||
// (mmterrain/src/terrain.c, built here for the host and loaded into luajit through the
|
||||
// FFI) gives the same mesh within float32 rounding, and the Lua settings side
|
||||
// (modules/moon/terrain.lua: random draws, traits, resolve, the strip) matches too.
|
||||
// Every seed runs with the default settings and a few others (each technique, many
|
||||
// craters, no ridges or patches). Needs node, luajit and a C compiler (cc).
|
||||
//
|
||||
// node scripts/moon-check.mjs [resolution] [seeds...]
|
||||
//
|
||||
// terrain.js is read from $MM_SERVER (default ../server).
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
@@ -13,34 +18,112 @@ const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||
const server = process.env.MM_SERVER || path.join(root, "..", "server");
|
||||
const T = await import(pathToFileURL(path.join(server, "web/static/js/terrain.js")));
|
||||
const [res = "32", ...rest] = process.argv.slice(2);
|
||||
const seeds = rest.length ? rest : ["1042", "1187", "1301", "1555", "29dea5d5-709d-4c55-a269-cd90e4379fe9"];
|
||||
// The fixed seeds, plus moon-id-like ones (deterministic).
|
||||
const ids = [];
|
||||
{
|
||||
let h = 0x9e3779b9;
|
||||
const next = () => ((h = (Math.imul(h ^ (h >>> 15), 0x2c1b3c6d) + 0x6d2b79f5) >>> 0), h.toString(16).padStart(8, "0"));
|
||||
for (let i = 0; i < 24; i++) ids.push(`${next()}-${next().slice(0, 4)}-4${next().slice(0, 3)}-a${next().slice(0, 3)}-${next()}${next().slice(0, 4)}`);
|
||||
}
|
||||
const seeds = rest.length ? rest : ["1042", "1187", "1301", "1555", "29dea5d5-709d-4c55-a269-cd90e4379fe9", ...ids];
|
||||
const fixed = (t) => ({ technique: t, variation: { technique: false } });
|
||||
const SETTINGS = [
|
||||
{}, fixed("fbm"), fixed("ridged"), fixed("warped"), fixed("billow"), fixed("craterField"),
|
||||
{ craters: { count: 1500, smoothness: 0, maxRadius: 0.4 } }, { ridge: { strength: 0 }, shading: { patchAmount: 0 } },
|
||||
];
|
||||
// Each case: a seed and a settings index (the default settings for every seed, the
|
||||
// others over a rotating subset).
|
||||
const cases = [];
|
||||
seeds.forEach((seed, i) => {
|
||||
cases.push([seed, 0]);
|
||||
for (let s = 1; s < SETTINGS.length; s++) if ((i + s) % 4 === 0 || rest.length) cases.push([seed, s]);
|
||||
});
|
||||
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mmterrain-"));
|
||||
const lib = path.join(dir, "libmmterrain.so");
|
||||
execFileSync("cc", ["-O2", "-std=c99", "-shared", "-fPIC", "-o", lib, path.join(root, "mmterrain/src/terrain.c"), "-lm"]);
|
||||
|
||||
const luaValue = (v) => (typeof v === "object" ? "{" + Object.entries(v).map(([k, x]) => `${k}=${luaValue(x)}`).join(",") + "}" : JSON.stringify(v));
|
||||
const lua = `
|
||||
package.path = ${JSON.stringify(root + "/?.lua;")} .. package.path
|
||||
local T = require("modules.moon.terrain")
|
||||
local SETTINGS = { ${SETTINGS.map(luaValue).join(", ")} }
|
||||
local ffi = require("ffi")
|
||||
ffi.cdef[[
|
||||
typedef struct mt_params mt_params;
|
||||
typedef struct mt_gen mt_gen;
|
||||
int mt_params_from_array(const double* a, int n, mt_params* p);
|
||||
mt_gen* mt_new(const mt_params* p);
|
||||
void mt_free(mt_gen* g);
|
||||
int mt_step(mt_gen* g, double budget);
|
||||
int mt_count(const mt_gen* g);
|
||||
int mt_n(const mt_gen* g);
|
||||
const double* mt_positions(const mt_gen* g);
|
||||
const double* mt_normals(const mt_gen* g);
|
||||
const double* mt_colors(const mt_gen* g);
|
||||
int mt_strip_len(int n);
|
||||
void mt_strip(int n, int* out);
|
||||
void mt_fill(const mt_gen* g, float* pos, float* nor, float* col, int stride, int from, int to);
|
||||
]]
|
||||
local N = ffi.load(${JSON.stringify(lib)})
|
||||
local function num(x) return string.format("%.17g", x) end
|
||||
local function arr(a) local o = {} for k = 1, #a do o[k] = num(a[k]) end return "[" .. table.concat(o, ",") .. "]" end
|
||||
local function carr(p, n) local o = {} for k = 0, n - 1 do o[k + 1] = num(p[k]) end return "[" .. table.concat(o, ",") .. "]" end
|
||||
local out = {}
|
||||
for i = 2, #arg do -- arg[1]: resolution, then the seeds
|
||||
local seed = arg[i]
|
||||
local s = T.merge_settings({})
|
||||
for i = 2, #arg, 2 do -- arg[1]: resolution, then seed, settings index pairs
|
||||
local seed, si = arg[i], tonumber(arg[i + 1])
|
||||
local s = T.merge_settings(SETTINGS[si + 1])
|
||||
local r = T.rng(T.hash_string(seed))
|
||||
local draws = {} for k = 1, 16 do draws[k] = num(r()) end
|
||||
local tr = T.traits(s, seed)
|
||||
local p = T.resolve(s, seed)
|
||||
p.resolution = tonumber(arg[1])
|
||||
local m = T.generate(p)
|
||||
out[#out + 1] = string.format('{"draws":[%s],"traits":[%q,%q,%s,%s,%s,%s],"positions":%s,"normals":%s,"colors":%s}',
|
||||
do
|
||||
local a = T.native_params(p)
|
||||
local buf = ffi.new("double[?]", #a, a)
|
||||
local params = ffi.new("char[1024]") -- (larger than mt_params)
|
||||
assert(N.mt_params_from_array(buf, #a, ffi.cast("mt_params*", params)) == 1)
|
||||
local g = N.mt_new(ffi.cast("mt_params*", params))
|
||||
local steps = 0
|
||||
repeat steps = steps + 1 until N.mt_step(g, 0.0005) == 1
|
||||
local c = N.mt_count(g) * 3
|
||||
-- The strip: the native order equals the Lua's (terrain.strip), and its fill writes
|
||||
-- those vertices.
|
||||
local strip = T.strip(N.mt_n(g))
|
||||
local len = N.mt_strip_len(N.mt_n(g))
|
||||
local sidx = ffi.new("int[?]", len)
|
||||
N.mt_strip(N.mt_n(g), sidx)
|
||||
local same = len == #strip
|
||||
for k = 1, #strip do if not same then break end same = sidx[k - 1] == strip[k] end
|
||||
local fill = ffi.new("float[?]", len * 9)
|
||||
local half = math.floor(len / 2) -- (in two ranges, like the web's chunks)
|
||||
N.mt_fill(g, fill, fill + 3, fill + 6, 9, 0, half)
|
||||
N.mt_fill(g, fill, fill + 3, fill + 6, 9, half, len)
|
||||
local P = N.mt_positions(g)
|
||||
local fdiff = 0
|
||||
for k = 1, #strip do
|
||||
local v = strip[k]
|
||||
for d = 0, 2 do fdiff = math.max(fdiff, math.abs(fill[(k - 1) * 9 + d] - P[v * 3 + d])) end
|
||||
end
|
||||
out[#out + 1] = string.format('{"draws":[%s],"traits":[%q,%q,%s,%s,%s,%s],"positions":%s,"normals":%s,"colors":%s,"strip":%s,"fill":%s,"steps":%d}',
|
||||
table.concat(draws, ","), tr.technique, tr.mid, num(tr.shapeStrength), num(tr.ridgeStrength), num(tr.craterCount),
|
||||
num(tr.maxRadius), arr(m.positions), arr(m.normals), arr(m.colors))
|
||||
num(tr.maxRadius), carr(N.mt_positions(g), c), carr(N.mt_normals(g), c), carr(N.mt_colors(g), c),
|
||||
same and "true" or "false", num(fdiff), steps)
|
||||
N.mt_free(g)
|
||||
end
|
||||
end
|
||||
print("[" .. table.concat(out, ",") .. "]")
|
||||
`;
|
||||
const got = JSON.parse(execFileSync("luajit", ["-", res, ...seeds], { input: lua, maxBuffer: 1 << 30 }).toString());
|
||||
const got = JSON.parse(execFileSync("luajit", ["-", res, ...cases.flat().map(String)], { input: lua, maxBuffer: 1 << 30 }).toString());
|
||||
|
||||
const maxDiff = (a, b) => (a.length !== b.length ? Infinity : a.reduce((d, v, k) => Math.max(d, Math.abs(v - b[k])), 0));
|
||||
const close = (m, g) => {
|
||||
const d = { positions: maxDiff(m.positions, g.positions), normals: maxDiff(m.normals, g.normals), colors: maxDiff(m.colors, g.colors) };
|
||||
return [d.positions < 1e-5 && d.normals < 1e-4 && d.colors < 1e-4, d];
|
||||
};
|
||||
const fmt = (d) => `positions ${d.positions.toExponential(1)}, normals ${d.normals.toExponential(1)}, colors ${d.colors.toExponential(1)}`;
|
||||
let failed = false;
|
||||
seeds.forEach((seed, i) => {
|
||||
const s = T.mergeSettings({});
|
||||
cases.forEach(([seed, si], i) => {
|
||||
const s = T.mergeSettings(SETTINGS[si]);
|
||||
const r = T.rng(T.hashString(seed));
|
||||
const draws = Array.from({ length: 16 }, () => r());
|
||||
const t = T.moonTraits(s, seed);
|
||||
@@ -48,15 +131,13 @@ seeds.forEach((seed, i) => {
|
||||
const p = T.resolveMoon(s, seed);
|
||||
p.resolution = Number(res);
|
||||
const m = T.generateMoon(p);
|
||||
const maxDiff = (a, b) => (a.length !== b.length ? Infinity : a.reduce((d, v, k) => Math.max(d, Math.abs(v - b[k])), 0));
|
||||
const d = {
|
||||
positions: maxDiff(m.positions, got[i].positions),
|
||||
normals: maxDiff(m.normals, got[i].normals),
|
||||
colors: maxDiff(m.colors, got[i].colors),
|
||||
};
|
||||
const exact = JSON.stringify(draws) === JSON.stringify(got[i].draws) && JSON.stringify(traits) === JSON.stringify(got[i].traits);
|
||||
const ok = exact && d.positions < 1e-5 && d.normals < 1e-4 && d.colors < 1e-4;
|
||||
const nat = got[i];
|
||||
const [natOk, d] = close(m, nat);
|
||||
const ok = exact && natOk && nat.strip && nat.fill < 1e-5;
|
||||
const line = `draws and traits ${exact ? "identical" : "DIFFER"} | native: ${fmt(d)}; strip ${nat.strip ? "same" : "DIFFERS"}, fill ${nat.fill.toExponential(1)}, ${nat.steps} steps`;
|
||||
failed ||= !ok;
|
||||
console.log(`${ok ? "ok " : "FAIL"} ${seed} ${t.technique}: draws and traits ${exact ? "identical" : "DIFFER"}, max diff positions ${d.positions.toExponential(1)}, normals ${d.normals.toExponential(1)}, colors ${d.colors.toExponential(1)}`);
|
||||
console.log(`${ok ? "ok " : "FAIL"} ${seed} [settings ${si}] ${t.technique}: ${line}`);
|
||||
});
|
||||
console.log(`${cases.length} cases at resolution ${res}: ${failed ? "FAILED" : "all ok"}`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
Reference in new issue
Block a user