Compare commits

..
8 Commits
11 changed files with 606 additions and 15 deletions

No files matched your search

+35 -3
View File
@@ -239,7 +239,9 @@ the markup colors). Fields refuse `<` and `&`. Measuring (`ui.text_width`,
components.
- `mmwindow/`: our native extension, `mmwindow.set_fullscreen(on)` / `is_fullscreen()`
(the engine's GLFW window to its monitor and back; Defold has no runtime fullscreen).
Changing it rebuilds the engine on the build server.
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`).
- `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),
`chart.lua`
@@ -298,12 +300,42 @@ file),
- `scripts/`: `shot.sh` (headless screenshot), `views.sh` (every section, any build),
`fixture.py` (a test player), `drive.py` + `scenarios/` (test driver), `dev.py` (hot
reload), `check.py` (make check), `refinery-play.sh`, `moon-check.mjs`,
`zzfx-render.mjs`.
`zzfx-render.mjs`, `serve-web.py` and `web-shots.mjs` (Web build).
## Web build
`make web` bundles for the browser (`wasm-web`, release; `WEB_VARIANT=debug` keeps logs
and the test hook) into `dist/web/MoonMiners/` (release ~4.3 MB, ~2.4 MB gzipped: engine wasm 2.9 MB,
archive 1.1 MB, page and loader).
`make serve-web` (`scripts/serve-web.py`, `UPSTREAM=` default http://localhost:3000,
`WEB_PORT=` 8080) serves it and proxies `/api/*` and `/static/*` (everything the client
fetches) to the server, so the game talks to its own origin; the server stays unchanged.
- The page (`web/index.html`): the canvas fills the window in device pixels and follows
its size (the layout is native-pixel like desktop); dark loading screen; "WebGL 2
needed" without WebGL 2 (the moon's shaders don't build for WebGL 1). `[html5]` in
game.project: `stretch`, no Defold buttons or banner, heap 256 MB.
- On html5 (`if html5 then`): `api.base` is the page's origin (`?api=<url>` picks another
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 12 ms a frame, a few
seconds per moon. Moon targets fall back to 8 bits without EXT_color_buffer_float.
- WebGL: every sampler of a bound program must not be the framebuffer being drawn
("feedback loop", the draw is dropped), and Firefox aborts on resizing a target with a
depth buffer: the render script binds every bloom sampler and makes targets again at a
new size. Check the console after render changes.
- Test: `make web WEB_VARIANT=debug`, `scripts/serve-web.py --port 8161 --upstream <dev
server>`, then `node scripts/web-shots.mjs http://127.0.0.1:8161/ fx.json <out>
[chromium|firefox] [WxH]` (Playwright; fx.json from `scripts/fixture.py`): every section
by real mouse clicks (element rects from the test hook: the page pushes commands onto
`window.mmTestQueue`, replies are `mm-test` console lines), the fullscreen button, a
claim; fails on console errors.
## 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).
- `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=`,
`POPUP=1` (a dialog open), `SETTLE=5`, `SCREEN=1920x1080 make shot
ARGS="--config=mm.window=1920x1080"`. Log in `dist/shot.log`. Give 2560x1600
@@ -429,7 +461,7 @@ against `MM_API` (default http://localhost:3000) when it answers; otherwise the
`dist/drive/*/game.log` is checked. `scripts/check.py --static` skips it.
## Config (`[mm]` in game.project, or `--config=mm.<key>=<value>` at launch)
- `api_base`: server URL (default `http://localhost:3000`). The player id is saved per
- `api_base`: server URL (default `http://localhost:3000`; on the web the page's origin). The player id is saved per
server.
- `new_player`: `1` forgets the saved player on start. `player`: play as this id.
- `autoplay`: `1` runs the scripted play-through (`make play`); `autoplay_script=refinery`.
+14 -1
View File
@@ -11,7 +11,7 @@ SHOT ?= dist/shot.png
# Extra engine args, e.g. ARGS="--config=mm.api_base=http://localhost:3001"
ARGS ?=
.PHONY: build run shot play clean resolve dev reload dev-stop drive check
.PHONY: build run shot play clean resolve dev reload dev-stop drive check web serve-web
$(BOB):
mkdir -p tools
@@ -29,6 +29,19 @@ build: $(BOB) .internal/lib/dependencies.json
java -jar $(BOB) --platform $(PLATFORM) --architectures $(PLATFORM) --variant $(VARIANT) \
--archive --bundle-output dist/$(PLATFORM) build bundle
# Web build (WebAssembly) into dist/web/MoonMiners/ (index.html + engine + archive). Release
# by default; WEB_VARIANT=debug keeps logs and the test hook (window.mmTestQueue).
# `make serve-web` serves it on :8080 with /api and /static proxied to UPSTREAM.
WEB_VARIANT ?= release
UPSTREAM ?= http://localhost:3000
WEB_PORT ?= 8080
web: $(BOB) .internal/lib/dependencies.json
java -jar $(BOB) --platform wasm-web --architectures wasm-web --variant $(WEB_VARIANT) \
--archive --bundle-output dist/web build bundle
serve-web:
scripts/serve-web.py --port $(WEB_PORT) --upstream $(UPSTREAM)
# Hot reload loop: `make dev` starts the game from build/default (once), `make reload`
# rebuilds (no bundle) and posts what changed to the running engine. HEADLESS=1 make dev
# runs it under Xvfb. See AGENTS.md "Fast iteration".
+8
View File
@@ -56,3 +56,11 @@ view =
[sound]
max_component_count = 64
[html5]
htmlfile = /web/index.html
scale_mode = stretch
show_fullscreen_button = 0
show_made_with_defold = 0
show_console_banner = 0
heap_size = 256
+11 -1
View File
@@ -8,11 +8,21 @@
-- visit; it is kept with sys.save so the same player comes back.
--
-- Base URL: game.project [mm] api_base (default http://localhost:3000), overridable at
-- launch with --config=mm.api_base=http://host:port.
-- launch with --config=mm.api_base=http://host:port. In a browser: the page's origin.
local M = {}
M.base = sys.get_config_string("mm.api_base", "http://localhost:3000"):gsub("/+$", "")
-- In a browser (the web build) the page's own origin: the browser only lets the game talk
-- to it (scripts/serve-web.py serves the game there and proxies the server's paths). A
-- page URL with ?api=<url> picks another server (it must allow cross-origin requests).
if html5 then
local ok, origin = pcall(html5.run,
"(new URLSearchParams(location.search).get('api') || location.origin).replace(/\\/+$/, '')")
if ok and type(origin) == "string" and origin:match("^https?://") then
M.base = origin
end
end
M.timeout = 10
-- Debug: mm.fake_latency_ms delays every request by half of it and its response by the
+29 -5
View File
@@ -9,7 +9,8 @@
-- screen, window.get_display_scale(); high_dpi is on, so window sizes are physical pixels;
-- --config=mm.display_scale=2 pretends one). screens pick changes up on their next update.
--
-- Fullscreen: the mmwindow native extension (mmwindow/), saved with the UI scale; F11 and
-- Fullscreen: the mmwindow native extension (mmwindow/), saved with the UI scale (in a
-- browser: the Fullscreen API, not saved); F11 and
-- the display settings (screens/game/settings.lua) toggle it.
--
-- A screen's "root" node keeps adjust FIT against 1280x800 (display.width/height); M.fit(root)
@@ -81,16 +82,39 @@ end
---------------------------------------------------------------- fullscreen
function M.can_fullscreen()
return mmwindow ~= nil
-- In a browser (html5) the page's Fullscreen API instead: the browser only allows it
-- shortly after a click or key press (fine from an input handler), so the saved state isn't
-- applied at start and isn't saved; the browser's own F11 works too (the canvas fills the
-- window either way).
local function js(code)
local ok, r = pcall(html5.run, code)
return ok and r or ""
end
function M.can_fullscreen()
return html5 ~= nil or mmwindow ~= nil
end
local web_wanted, web_wanted_at -- the last request (the browser answers a moment later)
function M.fullscreen()
if html5 then
if web_wanted ~= nil and socket.gettime() - web_wanted_at < 1 then
return web_wanted
end
return js("document.fullscreenElement ? '1' : ''") == "1"
end
return mmwindow ~= nil and mmwindow.is_fullscreen()
end
-- Fullscreen on or off; saved.
-- Fullscreen on or off; saved (not in a browser).
function M.set_fullscreen(on)
if html5 then
web_wanted, web_wanted_at = on, socket.gettime()
js(on and "document.documentElement.requestFullscreen && document.documentElement.requestFullscreen().catch(function(){})"
or "document.fullscreenElement && document.exitFullscreen().catch(function(){})")
return
end
if mmwindow then
mmwindow.set_fullscreen(on)
end
@@ -105,7 +129,7 @@ end
-- At start (main.script): the saved fullscreen, unless the window size is forced
-- (--config=mm.window, tests and shots).
function M.apply_saved()
if M.want_fullscreen and mmwindow and sys.get_config_string("mm.window", "") == "" then
if M.want_fullscreen and mmwindow and not html5 and sys.get_config_string("mm.window", "") == "" then
mmwindow.set_fullscreen(true)
end
end
+12
View File
@@ -451,6 +451,18 @@ function M.update(g, input, api)
end
return
end
-- In a browser (debug web build) there's no engine service: the page pushes commands
-- ({id, cmd, args}) onto window.mmTestQueue; replies are the same console lines.
if html5 then
local ok, s = pcall(html5.run, "window.mmTestQueue && window.mmTestQueue.length ? JSON.stringify(window.mmTestQueue.splice(0)) : ''")
if ok and s ~= "" then
local dok, list = pcall(json.decode, s)
for _, c in ipairs(dok and list or {}) do
c.args = c.args or {}
table.insert(_G.mm_test_queue, c)
end
end
end
local q = _G.mm_test_queue
if #q == 0 then
return
+7 -1
View File
@@ -24,7 +24,8 @@ 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 = 0.006 -- seconds of generation work per frame
local BUDGET = html5 and 0.012 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)
local CACHE_SIZE = 6
local SUPERSAMPLE = 2 -- the scene renders at 2x and is averaged down (antialiasing)
local CHARGE = 1.1 -- reveal: charge-up before the burst (s)
@@ -183,7 +184,12 @@ 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)
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()
+30 -4
View File
@@ -10,16 +10,36 @@ local moon = require("modules.moon.moon")
--
-- moon pass
--
-- An HDR (half float) render target, kept and resized as needed; `depth` adds a depth buffer.
-- An HDR (half float) render target, kept and made again at a new size; `depth` adds a depth buffer.
-- Without half float targets (graphics leaves the constant out; in a browser WebGL 2 can
-- sample them but draws into them only with EXT_color_buffer_float) an 8-bit one: the moon
-- still shows, with less bloom range.
local HDR_FORMAT = graphics.TEXTURE_FORMAT_RGBA16F
if HDR_FORMAT and html5 then
local ok, has = pcall(html5.run, "(function(){var c=document.createElement('canvas').getContext('webgl2');"
.. "return c&&c.getExtension('EXT_color_buffer_float')?'1':''})()")
if not (ok and has == "1") then
HDR_FORMAT = nil
end
end
HDR_FORMAT = HDR_FORMAT or graphics.TEXTURE_FORMAT_RGBA
if HDR_FORMAT == graphics.TEXTURE_FORMAT_RGBA then
print("render: no half float render targets, the moon renders in 8 bits")
end
local function hdr_target(self, name, w, h, depth)
local t = self.targets[name]
if t and t.w == w and t.h == h then
return t.rt
end
-- A new size makes a new target: resizing one with a depth buffer in place
-- (set_render_target_size) leaves it incomplete for a moment, which WebGL in Firefox
-- reports as an error, and the engine aborts.
if t then
render.set_render_target_size(t.rt, w, h)
else
local params = { [graphics.BUFFER_TYPE_COLOR0_BIT] = { format = graphics.TEXTURE_FORMAT_RGBA16F,
render.delete_render_target(t.rt)
t = nil
end
do
local params = { [graphics.BUFFER_TYPE_COLOR0_BIT] = { format = HDR_FORMAT,
width = w, height = h, min_filter = graphics.TEXTURE_FILTER_LINEAR, mag_filter = graphics.TEXTURE_FILTER_LINEAR,
u_wrap = graphics.TEXTURE_WRAP_CLAMP_TO_EDGE, v_wrap = graphics.TEXTURE_WRAP_CLAMP_TO_EDGE } }
if depth then
@@ -53,6 +73,12 @@ local function draw_bloom(self, f, scene_rt)
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)
-- Every sampler of moon_post gets a texture that no bloom pass draws into: the units
-- otherwise keep the targets bound last frame, and WebGL refuses a draw whose program
-- samples its own framebuffer ("feedback loop"), even from an unused sampler.
for i = 1, #BLOOM_KERNELS - 1 do
render.enable_texture("tex" .. i, scene_rt, graphics.BUFFER_TYPE_COLOR0_BIT)
end
render.enable_texture("tex0", scene_rt, graphics.BUFFER_TYPE_COLOR0_BIT)
pass(bright, w, h)
local input = bright
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""Serve the web build (make web) and proxy the game server's paths to it, so the game
talks to its own origin (the browser allows nothing else without CORS).
scripts/serve-web.py [--port 8080] [--upstream http://localhost:3000] [--dir dist/web/MoonMiners]
/api/* (the JSON API) and /static/* (catalog images, moon settings) go to the upstream
with their method, headers and body; responses come back as they are (status, body,
X-MM-User, Set-Cookie). Everything else is a file from --dir. Ctrl-C stops it.
"""
import argparse
import http.client
import os
import sys
import urllib.parse
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
PROXIED = ("/api/", "/static/")
# Hop-by-hop headers (and ones the proxy sets itself) aren't forwarded.
SKIP_REQ = {"host", "connection", "keep-alive", "proxy-connection", "te", "trailer",
"transfer-encoding", "upgrade", "accept-encoding", "content-length"}
SKIP_RESP = {"connection", "keep-alive", "transfer-encoding", "upgrade", "content-length",
"server", "date"}
def make_handler(root, upstream):
up = urllib.parse.urlsplit(upstream)
conn_cls = http.client.HTTPSConnection if up.scheme == "https" else http.client.HTTPConnection
up_host = up.netloc
class Handler(SimpleHTTPRequestHandler):
protocol_version = "HTTP/1.1"
extensions_map = {**SimpleHTTPRequestHandler.extensions_map,
".wasm": "application/wasm", ".js": "text/javascript",
".json": "application/json", "": "application/octet-stream"}
def __init__(self, *a, **kw):
super().__init__(*a, directory=root, **kw)
def log_message(self, fmt, *args):
sys.stderr.write("%s %s\n" % (self.address_string(), fmt % args))
def proxied(self):
return self.path.startswith(PROXIED)
def end_headers(self):
if not self.proxied():
# Always the newest build (no stale engine after make web).
self.send_header("Cache-Control", "no-cache")
super().end_headers()
def proxy(self):
length = int(self.headers.get("Content-Length") or 0)
body = self.rfile.read(length) if length else None
headers = {k: v for k, v in self.headers.items() if k.lower() not in SKIP_REQ}
headers["Host"] = up_host
headers["X-Forwarded-For"] = self.client_address[0]
path = (up.path.rstrip("/") + self.path) if up.path else self.path
try:
conn = conn_cls(up_host, timeout=30)
conn.request(self.command, path, body=body, headers=headers)
resp = conn.getresponse()
data = resp.read()
except OSError as e:
msg = ('{"error": "upstream %s unreachable: %s"}' % (upstream, e)).encode()
self.send_response(502)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(msg)))
self.end_headers()
self.wfile.write(msg)
return
self.send_response(resp.status, resp.reason)
for k, v in resp.getheaders():
if k.lower() not in SKIP_RESP:
self.send_header(k, v)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
if self.command != "HEAD":
self.wfile.write(data)
conn.close()
def do_GET(self):
if self.proxied():
return self.proxy()
super().do_GET()
def do_HEAD(self):
if self.proxied():
return self.proxy()
super().do_HEAD()
def do_POST(self):
if self.proxied():
return self.proxy()
self.send_error(405)
do_PUT = do_DELETE = do_PATCH = do_POST
return Handler
def main():
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--port", type=int, default=8080)
p.add_argument("--bind", default="127.0.0.1")
p.add_argument("--upstream", default="http://localhost:3000")
p.add_argument("--dir", default=os.path.join(here, "dist", "web", "MoonMiners"))
a = p.parse_args()
if not os.path.isfile(os.path.join(a.dir, "index.html")):
sys.exit("serve-web: no build in %s (make web)" % a.dir)
srv = ThreadingHTTPServer((a.bind, a.port), make_handler(a.dir, a.upstream.rstrip("/")))
srv.daemon_threads = True
print("serve-web: http://%s:%d (game from %s, /api and /static from %s)" % (a.bind, a.port, a.dir, a.upstream), flush=True)
try:
srv.serve_forever()
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()
+169
View File
@@ -0,0 +1,169 @@
// Headless browser check of the web build (make web WEB_VARIANT=debug, served by
// scripts/serve-web.py): loads the game as a fixture player, clicks through every section
// with the browser's own mouse (element rects from the test hook, window.mmTestQueue),
// saves a screenshot of each and fails on console errors.
//
// node scripts/web-shots.mjs <page url> <fixture.json> <out dir> [chromium|firefox] [WxH]
//
// Needs Playwright and its browsers (npm i playwright; PLAYWRIGHT=<its folder> if it's
// installed elsewhere).
// WebGL runs on SwiftShader (Chromium) or software WebRender (Firefox). Silent.
import { createRequire } from "node:module";
import fs from "node:fs";
const require = createRequire(import.meta.url);
const { chromium, firefox } = require(process.env.PLAYWRIGHT || "playwright");
const [url, fixtureFile, out, browserName = "chromium", size = "1280x800"] = process.argv.slice(2);
const [W, H] = size.split("x").map(Number);
const fx = JSON.parse(fs.readFileSync(fixtureFile, "utf8"));
fs.mkdirSync(out, { recursive: true });
const browser = browserName === "firefox"
? await firefox.launch({ firefoxUserPrefs: { "webgl.force-enabled": true, "media.volume_scale": "0.0", "media.autoplay.default": 0 } })
: await chromium.launch({ args: ["--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist", "--mute-audio", "--autoplay-policy=no-user-gesture-required"] });
const context = await browser.newContext({ viewport: { width: W, height: H }, deviceScaleFactor: 1 });
const origin = new URL(url).origin;
// The player: the server's cookie (the first response echoes it as X-MM-User, which the
// game saves like on desktop).
await context.addCookies([{ name: "mm_user", value: fx.player, url: origin }]);
const page = await context.newPage();
const errors = [];
const replies = new Map();
let parts = [];
const log = [];
page.on("console", (m) => {
const t = m.text().replace(/\n$/, ""); // (engine log lines end in a newline)
log.push(m.type() + ": " + t);
if (m.type() === "error") errors.push(t);
if (/ERROR:/.test(t)) errors.push(t);
let r;
if ((r = t.match(/(?:^|: )mm-test-part \d+ (.*)$/s))) parts.push(r[1]);
else if ((r = t.match(/(?:^|: )mm-test (\d+) (.*)$/s))) {
replies.set(Number(r[1]), parts.join("") + r[2]);
parts = [];
}
});
page.on("pageerror", (e) => errors.push("pageerror: " + e.message));
let nextId = 1;
async function cmd(name, ...args) {
const id = nextId++;
await page.evaluate((c) => { (window.mmTestQueue = window.mmTestQueue || []).push(c); }, { id, cmd: name, args });
const t0 = Date.now();
while (!replies.has(id)) {
if (Date.now() - t0 > 60000) throw new Error("no reply to " + name + " " + args.join(" "));
await page.waitForTimeout(50);
}
const res = JSON.parse(replies.get(id));
if (!res.ok) throw new Error(name + " " + args.join(" ") + ": " + res.error);
return res;
}
const idle = () => cmd("wait_for", "st and not g.busy and not g.dirty", "40");
// A real click (the browser's mouse) on a registered element's center.
async function click(id) {
await cmd("scroll_to", id).catch(() => {}); // (only elements in a scroll area)
await page.waitForTimeout(300);
const { elements } = await cmd("list");
const e = elements.find((x) => (x.id === id || x.id.startsWith(id)) && x.rect && x.visible);
if (!e) throw new Error("no visible element " + id);
const dpr = await page.evaluate(() => window.devicePixelRatio || 1);
const ch = await page.evaluate(() => document.getElementById("canvas").height);
const x = e.rect.x / dpr, y = (ch - e.rect.y) / dpr;
await page.mouse.move(x, y);
await page.waitForTimeout(80);
await page.mouse.down();
await page.waitForTimeout(80);
await page.mouse.up();
console.log(" click", e.id, Math.round(x), Math.round(y));
}
async function shot(name) {
await page.waitForTimeout(1500);
await page.screenshot({ path: `${out}/${browserName}-${size}-${name}.png` });
console.log(" shot", name);
}
async function run() {
const t0 = Date.now();
await page.goto(url);
await page.waitForFunction(() => document.getElementById("loading").classList.contains("done"), null, { timeout: 120000 });
await idle();
console.log("loaded in", (Date.now() - t0) / 1000, "s");
// The moon's mesh (generated in Lua over frames, slower on the web).
await cmd("wait_for", "require('modules.moon.moon').frame.has_mesh", "90");
console.log("moon shown after", (Date.now() - t0) / 1000, "s");
const info = await page.evaluate(() => {
const gl = document.createElement("canvas").getContext("webgl2");
return { webgl2: !!gl, colorBufferFloat: !!(gl && gl.getExtension("EXT_color_buffer_float")), heapMB: Math.round(((window.Module && Module.HEAP8 && Module.HEAP8.length) || 0) / 1048576) };
});
console.log("browser", info);
await cmd("navigate", `{section='moons', moon='${fx.moon}'}`);
await idle();
await shot("moons");
await click(`well:${fx.well}`);
await idle();
await shot("well");
for (const s of ["equipment", "store", "market", "refinery", "advisors", "leaderboard", "help"]) {
await click(`tab:${s}`);
await idle();
await shot(s);
}
await click("tab:equipment");
await idle();
await click(`drill:${fx.drill_mining}`);
await idle();
await shot("drill");
await click("nav:new-moon");
await idle();
await shot("new-moon");
// The display dialog: its fullscreen button goes through the browser's Fullscreen API.
await cmd("navigate", `{section='moons', moon='${fx.moon}'}`);
await idle();
await click("display");
await shot("display");
await click("dialog:fullscreen");
await page.waitForTimeout(1000);
info.fullscreen = await page.evaluate(() => !!document.fullscreenElement);
info.fullscreenSize = await page.evaluate(() => [innerWidth, innerHeight, document.getElementById("canvas").width, document.getElementById("canvas").height]);
console.log("fullscreen after click:", info.fullscreen, info.fullscreenSize);
await shot("fullscreen");
await click("dialog:fullscreen");
await page.waitForTimeout(1000);
info.fullscreenAfterLeave = await page.evaluate(() => !!document.fullscreenElement);
console.log("fullscreen after second click:", info.fullscreenAfterLeave);
await page.keyboard.press("Escape");
// A predicted action through a real click: claim a ready drill on the moon.
await idle();
const before = (await cmd("state")).state;
const { elements } = await cmd("list");
const claim = elements.find((e) => e.id.startsWith("act:claim") && e.visible);
if (claim) {
await click(claim.id);
await idle();
await page.waitForTimeout(1500);
const after = (await cmd("state")).state;
info.claim = { id: claim.id, before: before.resources, after: after.resources };
console.log("claim", JSON.stringify(info.claim));
await shot("claimed");
}
const wallet0 = (await cmd("state")).state;
fs.writeFileSync(`${out}/${browserName}-${size}-state.json`, JSON.stringify({ wallet0, info }, null, 1));
}
try {
await run();
} catch (e) {
errors.push("FAILED: " + e.message);
await page.screenshot({ path: `${out}/${browserName}-${size}-failed.png` }).catch(() => {});
}
fs.writeFileSync(`${out}/${browserName}-${size}-console.log`, log.join("\n"));
await browser.close();
if (errors.length) {
console.log("CONSOLE ERRORS:\n" + errors.join("\n"));
process.exit(1);
}
console.log("ok, no console errors");
+168
View File
@@ -0,0 +1,168 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, minimal-ui, shrink-to-fit=no, viewport-fit=cover">
<meta name="mobile-web-app-capable" content="yes">
<!-- Moon Miners web build (game.project [html5] htmlfile). Based on the pinned Defold
engine_template.html: the canvas fills the window (device pixels, high_dpi) and
follows its size; a dark loading screen in the theme's colors; a clear message
when WebGL 2 is missing (the moon's shaders don't build for WebGL 1). -->
<link rel="icon" href="data:,">
<link rel="preload" as="fetch" crossorigin href="{{DEFOLD_ARCHIVE_LOCATION_PREFIX}}/archive_files.json{{DEFOLD_ARCHIVE_LOCATION_SUFFIX}}">
{{#DEFOLD_HAS_WASM_ENGINE}}
{{^DEFOLD_HAS_WASM_PTHREAD_ENGINE}}
<link rel="preload" as="fetch" crossorigin href="{{exe-name}}_wasm.js">
<link rel="preload" as="fetch" crossorigin href="{{exe-name}}.wasm">
{{/DEFOLD_HAS_WASM_PTHREAD_ENGINE}}
{{/DEFOLD_HAS_WASM_ENGINE}}
<title>{{project.title}}</title>
<style type="text/css">
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #040a16;
color: #e6f1ff;
font-family: "Rajdhani", "Segoe UI", system-ui, sans-serif;
}
body {
position: fixed; /* no overscroll */
inset: 0;
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
}
#app-container, #canvas-container {
position: absolute;
inset: 0;
overflow: hidden;
}
#canvas {
display: block;
width: 100%;
height: 100%;
outline: none;
border: 0;
}
#loading, .notice {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 18px;
background: #040a16;
transition: opacity .35s ease;
}
#loading.done { opacity: 0; pointer-events: none; }
.title {
font-size: 15px;
letter-spacing: .32em;
text-transform: uppercase;
color: #5fd4ff;
}
.track {
width: 220px;
height: 3px;
background: #12233d;
overflow: hidden;
}
.fill {
width: 100%;
height: 100%;
background: #5fd4ff;
box-shadow: 0 0 10px #5fd4ff;
transform-origin: left;
transform: scaleX(0);
transition: transform .25s ease;
}
.notice { display: none; text-align: center; padding: 0 24px; }
.notice h1 { margin: 0; font-size: 22px; letter-spacing: .12em; text-transform: uppercase; color: #ffb454; }
.notice p { margin: 0; max-width: 460px; line-height: 1.5; color: #9bb3cf; }
.notice a { color: #5fd4ff; }
</style>
</head>
<body>
<div id="app-container">
<div id="canvas-container">
<canvas id="canvas" tabindex="1" width="{{display.width}}" height="{{display.height}}"></canvas>
</div>
<div id="loading">
<div class="title">Moon Miners</div>
<div class="track"><div class="fill" id="loading-fill"></div></div>
</div>
<div class="notice" id="webgl-not-supported">
<h1>WebGL 2 needed</h1>
<p>Moon Miners needs WebGL 2, which this browser doesn't offer (or has turned off).
Try a current Chrome, Firefox, Edge or Safari, with hardware acceleration on.</p>
</div>
<div class="notice" id="start-error">
<h1>Couldn't start</h1>
<p id="start-error-text">The game failed to load. Reload the page to try again.</p>
</div>
<div class="notice" id="running-from-file-warning">
<h1>Open it from a server</h1>
<p>This page has to be served over HTTP (make serve-web), not opened as a file.</p>
</div>
</div>
<script>
if (typeof globalThis === 'undefined') { window.globalThis = window; }
</script>
<script id="engine-loader" type="text/javascript" src="dmloader.js"></script>
<script id="engine-setup" type="text/javascript">
(function() {
var loading = document.getElementById("loading");
var fill = document.getElementById("loading-fill");
function show(id) {
loading.classList.add("done");
document.getElementById(id).style.display = "flex";
}
// Our own loading screen instead of Defold's progress bar under the canvas.
ProgressView.addProgress = function() {};
ProgressView.updateProgress = function(p) {
fill.style.transform = "scaleX(" + Math.min(p, 100) / 100 + ")";
};
// Called when the engine starts (dmloader postRun / _callMain).
ProgressView.removeProgress = function() {
fill.style.transform = "scaleX(1)";
setTimeout(function() { loading.classList.add("done"); }, 150);
};
CUSTOM_PARAMETERS.start_error = function(error) {
if (error && error.message) {
document.getElementById("start-error-text").textContent = "The game failed to load (" + error.message + "). Reload the page to try again.";
}
show("start-error");
};
CUSTOM_PARAMETERS.unsupported_webgl_callback = function() { show("webgl-not-supported"); };
// Full window: the canvas takes the whole viewport, in device pixels; the game
// lays itself out at native pixel size (modules/screen.lua) like on desktop.
CUSTOM_PARAMETERS.resize_window_callback = function() {
var canvas = document.getElementById("canvas");
var dpi = window.devicePixelRatio || 1;
var w = Math.max(1, Math.floor(window.innerWidth * dpi));
var h = Math.max(1, Math.floor(window.innerHeight * dpi));
if (canvas.width !== w) canvas.width = w;
if (canvas.height !== h) canvas.height = h;
};
// Zoom (Ctrl +/-) changes devicePixelRatio without a resize on some browsers.
window.addEventListener("resize", function() { CUSTOM_PARAMETERS.resize_window_callback(); });
document.addEventListener("fullscreenchange", function() { CUSTOM_PARAMETERS.resize_window_callback(); });
})();
</script>
<script id="engine-start" type="text/javascript">
if (window.location.href.startsWith("file://")) {
document.getElementById("loading").classList.add("done");
document.getElementById("running-from-file-warning").style.display = "flex";
} else if (!document.createElement("canvas").getContext("webgl2")) {
CUSTOM_PARAMETERS.unsupported_webgl_callback();
} else {
EngineLoader.load("canvas", "{{exe-name}}");
}
</script>
</body>
</html>