Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80f874c452 | ||
|
|
8cfc646d78 | ||
|
|
1bcb5c88a2 | ||
|
|
52efc686cf | ||
|
|
604aee047a | ||
|
|
5556c24142 | ||
|
|
aa88e6f213 | ||
|
|
b27966e742 |
No files matched your search
+35
-3
@@ -239,7 +239,9 @@ the markup colors). Fields refuse `<` and `&`. Measuring (`ui.text_width`,
|
|||||||
components.
|
components.
|
||||||
- `mmwindow/`: our native extension, `mmwindow.set_fullscreen(on)` / `is_fullscreen()`
|
- `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).
|
(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
|
- `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),
|
by time on g and rebound every build; fx.lua asks `g.on_sale` for the coins' origin),
|
||||||
`chart.lua`
|
`chart.lua`
|
||||||
@@ -298,12 +300,42 @@ file),
|
|||||||
- `scripts/`: `shot.sh` (headless screenshot), `views.sh` (every section, any build),
|
- `scripts/`: `shot.sh` (headless screenshot), `views.sh` (every section, any build),
|
||||||
`fixture.py` (a test player), `drive.py` + `scenarios/` (test driver), `dev.py` (hot
|
`fixture.py` (a test player), `drive.py` + `scenarios/` (test driver), `dev.py` (hot
|
||||||
reload), `check.py` (make check), `refinery-play.sh`, `moon-check.mjs`,
|
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
|
## Commands
|
||||||
- `make build`: `bob build bundle` (debug) into `dist/x86_64-linux/MoonMiners/` (`build/` is
|
- `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 extension).
|
||||||
- `make run ARGS="--config=mm.api_base=http://localhost:3001"`: build and start.
|
- `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=`,
|
- `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
|
`POPUP=1` (a dialog open), `SETTLE=5`, `SCREEN=1920x1080 make shot
|
||||||
ARGS="--config=mm.window=1920x1080"`. Log in `dist/shot.log`. Give 2560x1600
|
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.
|
`dist/drive/*/game.log` is checked. `scripts/check.py --static` skips it.
|
||||||
|
|
||||||
## Config (`[mm]` in game.project, or `--config=mm.<key>=<value>` at launch)
|
## 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.
|
server.
|
||||||
- `new_player`: `1` forgets the saved player on start. `player`: play as this id.
|
- `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`.
|
- `autoplay`: `1` runs the scripted play-through (`make play`); `autoplay_script=refinery`.
|
||||||
|
|||||||
+14
-1
@@ -11,7 +11,7 @@ SHOT ?= dist/shot.png
|
|||||||
# Extra engine args, e.g. ARGS="--config=mm.api_base=http://localhost:3001"
|
# Extra engine args, e.g. ARGS="--config=mm.api_base=http://localhost:3001"
|
||||||
ARGS ?=
|
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):
|
$(BOB):
|
||||||
mkdir -p tools
|
mkdir -p tools
|
||||||
@@ -29,6 +29,19 @@ build: $(BOB) .internal/lib/dependencies.json
|
|||||||
java -jar $(BOB) --platform $(PLATFORM) --architectures $(PLATFORM) --variant $(VARIANT) \
|
java -jar $(BOB) --platform $(PLATFORM) --architectures $(PLATFORM) --variant $(VARIANT) \
|
||||||
--archive --bundle-output dist/$(PLATFORM) build bundle
|
--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`
|
# 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
|
# rebuilds (no bundle) and posts what changed to the running engine. HEADLESS=1 make dev
|
||||||
# runs it under Xvfb. See AGENTS.md "Fast iteration".
|
# runs it under Xvfb. See AGENTS.md "Fast iteration".
|
||||||
|
|||||||
@@ -56,3 +56,11 @@ view =
|
|||||||
|
|
||||||
[sound]
|
[sound]
|
||||||
max_component_count = 64
|
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
@@ -8,11 +8,21 @@
|
|||||||
-- visit; it is kept with sys.save so the same player comes back.
|
-- 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
|
-- 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 = {}
|
local M = {}
|
||||||
|
|
||||||
M.base = sys.get_config_string("mm.api_base", "http://localhost:3000"):gsub("/+$", "")
|
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
|
M.timeout = 10
|
||||||
|
|
||||||
-- Debug: mm.fake_latency_ms delays every request by half of it and its response by the
|
-- Debug: mm.fake_latency_ms delays every request by half of it and its response by the
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
-- screen, window.get_display_scale(); high_dpi is on, so window sizes are physical pixels;
|
-- 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.
|
-- --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.
|
-- 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)
|
-- A screen's "root" node keeps adjust FIT against 1280x800 (display.width/height); M.fit(root)
|
||||||
@@ -81,16 +82,39 @@ end
|
|||||||
|
|
||||||
---------------------------------------------------------------- fullscreen
|
---------------------------------------------------------------- fullscreen
|
||||||
|
|
||||||
function M.can_fullscreen()
|
-- In a browser (html5) the page's Fullscreen API instead: the browser only allows it
|
||||||
return mmwindow ~= nil
|
-- 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
|
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()
|
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()
|
return mmwindow ~= nil and mmwindow.is_fullscreen()
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Fullscreen on or off; saved.
|
-- Fullscreen on or off; saved (not in a browser).
|
||||||
function M.set_fullscreen(on)
|
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
|
if mmwindow then
|
||||||
mmwindow.set_fullscreen(on)
|
mmwindow.set_fullscreen(on)
|
||||||
end
|
end
|
||||||
@@ -105,7 +129,7 @@ end
|
|||||||
-- At start (main.script): the saved fullscreen, unless the window size is forced
|
-- At start (main.script): the saved fullscreen, unless the window size is forced
|
||||||
-- (--config=mm.window, tests and shots).
|
-- (--config=mm.window, tests and shots).
|
||||||
function M.apply_saved()
|
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)
|
mmwindow.set_fullscreen(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -451,6 +451,18 @@ function M.update(g, input, api)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
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
|
local q = _G.mm_test_queue
|
||||||
if #q == 0 then
|
if #q == 0 then
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ local FOV = math.rad(35)
|
|||||||
local RESUME = 1.5 -- seconds before auto-rotation resumes after a drag
|
local RESUME = 1.5 -- seconds before auto-rotation resumes after a drag
|
||||||
local ROTATE_SPEED = 0.6
|
local ROTATE_SPEED = 0.6
|
||||||
local DAMPING = 0.05
|
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 CACHE_SIZE = 6
|
||||||
local SUPERSAMPLE = 2 -- the scene renders at 2x and is averaged down (antialiasing)
|
local SUPERSAMPLE = 2 -- the scene renders at 2x and is averaged down (antialiasing)
|
||||||
local CHARGE = 1.1 -- reveal: charge-up before the burst (s)
|
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
|
-- A job: a coroutine that generates the moon and fills a buffer, yielding when the
|
||||||
-- frame's budget is spent.
|
-- 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)
|
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 }
|
local job = { id = id, key = key, params = params, started = now(), frames = 0, work = 0 }
|
||||||
job.co = coroutine.create(function()
|
job.co = coroutine.create(function()
|
||||||
local function checkpoint()
|
local function checkpoint()
|
||||||
|
|||||||
@@ -10,16 +10,36 @@ local moon = require("modules.moon.moon")
|
|||||||
--
|
--
|
||||||
-- moon pass
|
-- 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 function hdr_target(self, name, w, h, depth)
|
||||||
local t = self.targets[name]
|
local t = self.targets[name]
|
||||||
if t and t.w == w and t.h == h then
|
if t and t.w == w and t.h == h then
|
||||||
return t.rt
|
return t.rt
|
||||||
end
|
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
|
if t then
|
||||||
render.set_render_target_size(t.rt, w, h)
|
render.delete_render_target(t.rt)
|
||||||
else
|
t = nil
|
||||||
local params = { [graphics.BUFFER_TYPE_COLOR0_BIT] = { format = graphics.TEXTURE_FORMAT_RGBA16F,
|
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,
|
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 } }
|
u_wrap = graphics.TEXTURE_WRAP_CLAMP_TO_EDGE, v_wrap = graphics.TEXTURE_WRAP_CLAMP_TO_EDGE } }
|
||||||
if depth then
|
if depth then
|
||||||
@@ -53,6 +73,12 @@ local function draw_bloom(self, f, scene_rt)
|
|||||||
local bright = hdr_target(self, "bloom_bright", w, h)
|
local bright = hdr_target(self, "bloom_bright", w, h)
|
||||||
c.post = vmath.vector4(0, 0, 0, 0)
|
c.post = vmath.vector4(0, 0, 0, 0)
|
||||||
c.post2 = vmath.vector4(0, 0, b.z, 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)
|
render.enable_texture("tex0", scene_rt, graphics.BUFFER_TYPE_COLOR0_BIT)
|
||||||
pass(bright, w, h)
|
pass(bright, w, h)
|
||||||
local input = bright
|
local input = bright
|
||||||
|
|||||||
Executable
+123
@@ -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()
|
||||||
@@ -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");
|
||||||
@@ -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>
|
||||||
Reference in new issue
Block a user