Compare commits
4
Commits
0bf4779903
...
d2b4f62949
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b4f62949 | ||
|
|
0450ecd05a | ||
|
|
e9c1e344b8 | ||
|
|
32b3465c46 |
No files matched your search
+7
-2
@@ -163,6 +163,8 @@ Build new UI from these instead of coordinate math. Each file's header documents
|
|||||||
dialog's Druid exist only while it is open or closing, and a close ends within 1 s; a
|
dialog's Druid exist only while it is open or closing, and a close ends within 1 s; a
|
||||||
break is repaired and logged as `ERROR:MM: input invariant broken: ...` (check.py fails
|
break is repaired and logged as `ERROR:MM: input invariant broken: ...` (check.py fails
|
||||||
a log with `ERROR:MM`).
|
a log with `ERROR:MM`).
|
||||||
|
- `screens/game/grant.lua`: the quick grant dialog for admins (`state.admin`: header "+"
|
||||||
|
button, G key): item and amount pickers, then `admin-grant`.
|
||||||
- `panel.lua`: the `.frame` panel with a body group and the entrance (`p:enter`).
|
- `panel.lua`: the `.frame` panel with a body group and the entrance (`p:enter`).
|
||||||
- `card.lua`: image column + text column laid out with flex (below; measured text, `title`,
|
- `card.lua`: image column + text column laid out with flex (below; measured text, `title`,
|
||||||
`text`, `para`, `block` (fixed height, or measured: `block(fn)` with fn returning its
|
`text`, `para`, `block` (fixed height, or measured: `block(fn)` with fn returning its
|
||||||
@@ -307,6 +309,8 @@ 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).
|
||||||
|
On desktop it also makes the window's mouse buttons and keys sticky (GLFW), so a click
|
||||||
|
or key press shorter than a frame isn't lost (the engine polls their states once a frame).
|
||||||
Changing it rebuilds the engine on the build server. On the web it builds as stubs; the
|
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`).
|
browser's Fullscreen API is used instead (`modules/screen.lua`).
|
||||||
- `mmterrain/`: native extension, the moon generator in C (`src/terrain.c`, a port of
|
- `mmterrain/`: native extension, the moon generator in C (`src/terrain.c`, a port of
|
||||||
@@ -472,8 +476,9 @@ The game it starts saves its files under `<out>/data` (XDG_DATA_HOME, or
|
|||||||
build shows the engine's profiler.
|
build shows the engine's profiler.
|
||||||
Those go through the test hook, which calls the screen's input function directly, so
|
Those go through the test hook, which calls the screen's input function directly, so
|
||||||
they work even when the engine sends the game no input. Real input through the X server
|
they work even when the engine sends the game no input. Real input through the X server
|
||||||
(xdotool on the driver's Xvfb; key and button held 80 ms, since the engine polls once a
|
(xdotool on the driver's Xvfb, its own display from `Xvfb -displayfd`: `xvfb-run -a` raced
|
||||||
frame): `os_click(id | prefix)`, `os_move(x, y)`, `os_key("Escape")`, `os_point(id)`.
|
when two runs started together; key and button held 80 ms like a hand; the engine polls
|
||||||
|
once a frame, and mmwindow's sticky keys and buttons keep a press shorter than a frame): `os_click(id | prefix)`, `os_move(x, y)`, `os_key("Escape")`, `os_point(id)`.
|
||||||
`g.engine_inputs` counts the engine's own `on_input` calls, `g.builds` the rebuilds.
|
`g.engine_inputs` counts the engine's own `on_input` calls, `g.builds` the rebuilds.
|
||||||
- Run: `scripts/drive.py --api <server> [--fixture | --fixture-file fx.json] [--size
|
- Run: `scripts/drive.py --api <server> [--fixture | --fixture-file fx.json] [--size
|
||||||
2560x1600] [--config mm.x=y] <scenario> [args]`. It starts the engine from
|
2560x1600] [--config mm.x=y] <scenario> [args]`. It starts the engine from
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ local theme = require("modules.theme")
|
|||||||
local ui = require("modules.ui")
|
local ui = require("modules.ui")
|
||||||
local anim = require("modules.anim")
|
local anim = require("modules.anim")
|
||||||
local test = require("modules.test")
|
local test = require("modules.test")
|
||||||
local helper = require("druid.helper")
|
|
||||||
local druid_button = require("druid.base.button")
|
local druid_button = require("druid.base.button")
|
||||||
|
|
||||||
local S = theme.size
|
local S = theme.size
|
||||||
@@ -144,23 +143,28 @@ function M.release()
|
|||||||
carry.pressed = nil
|
carry.pressed = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- After a rebuild, with the mouse still down at (x, y) (screen) on a button of the build
|
-- before: press the new button(s) with its id, so the release clicks it (Druid checks the pointer then).
|
||||||
-- before: press the new button with its id under the pointer, so the release clicks it.
|
-- before: press the new button with its id under the pointer, so the release clicks it.
|
||||||
function M.carry(x, y)
|
function M.carry(x, y)
|
||||||
local p = carry.pressed
|
local p = carry.pressed
|
||||||
if not p or not p.id then
|
if not p or not p.id then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
-- (not only one under the pointer now: a full build plays the page's entrance, so the
|
||||||
|
-- new button may be elsewhere this frame; Druid checks the pointer at the release)
|
||||||
|
local found
|
||||||
for _, b in ipairs(carry.made) do
|
for _, b in ipairs(carry.made) do
|
||||||
if test.id_of(b.node) == p.id and gui.is_enabled(b.node, true)
|
if test.id_of(b.node) == p.id and gui.is_enabled(b.node, true) then
|
||||||
and helper.pick_node(b.node, x, y, b.click_zone) then
|
|
||||||
b.can_action = true
|
b.can_action = true
|
||||||
b.last_pressed_time = p.t
|
b.last_pressed_time = p.t
|
||||||
b.hover:set_hover(true)
|
b.hover:set_hover(true)
|
||||||
carry.pressed = { node = b.node, id = p.id, t = p.t }
|
found = b
|
||||||
return true
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if found then
|
||||||
|
carry.pressed = { node = found.node, id = p.id, t = p.t }
|
||||||
|
return true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Draw and bind. Returns { root, nodes, w, h, button }.
|
-- Draw and bind. Returns { root, nodes, w, h, button }.
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
// mmwindow.set_fullscreen(true | false)
|
// mmwindow.set_fullscreen(true | false)
|
||||||
// mmwindow.is_fullscreen() -> bool
|
// mmwindow.is_fullscreen() -> bool
|
||||||
//
|
//
|
||||||
|
// Also sticky mouse buttons and keys: the engine polls button and key states once a frame,
|
||||||
|
// so a press and release between two polls (a quick click or a touchpad tap at a low frame
|
||||||
|
// rate) was lost; sticky, a press released before the poll still reads as pressed once.
|
||||||
|
//
|
||||||
// The GLFW functions are the engine's (statically linked); declared here, no header.
|
// The GLFW functions are the engine's (statically linked); declared here, no header.
|
||||||
|
|
||||||
#define EXTENSION_NAME MMWindow
|
#define EXTENSION_NAME MMWindow
|
||||||
@@ -32,8 +36,12 @@ void glfwGetMonitorPos(GLFWmonitor* monitor, int* x, int* y);
|
|||||||
void glfwGetWindowPos(GLFWwindow* window, int* x, int* y);
|
void glfwGetWindowPos(GLFWwindow* window, int* x, int* y);
|
||||||
void glfwGetWindowSize(GLFWwindow* window, int* w, int* h);
|
void glfwGetWindowSize(GLFWwindow* window, int* w, int* h);
|
||||||
void glfwSetWindowMonitor(GLFWwindow* window, GLFWmonitor* monitor, int x, int y, int w, int h, int refresh);
|
void glfwSetWindowMonitor(GLFWwindow* window, GLFWmonitor* monitor, int x, int y, int w, int h, int refresh);
|
||||||
|
void glfwSetInputMode(GLFWwindow* window, int mode, int value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define MM_GLFW_STICKY_KEYS 0x00033002
|
||||||
|
#define MM_GLFW_STICKY_MOUSE_BUTTONS 0x00033003
|
||||||
|
|
||||||
static int g_X, g_Y, g_W, g_H;
|
static int g_X, g_Y, g_W, g_H;
|
||||||
static bool g_Saved = false;
|
static bool g_Saved = false;
|
||||||
|
|
||||||
@@ -128,9 +136,27 @@ static dmExtension::Result Initialize(dmExtension::Params* params)
|
|||||||
return dmExtension::RESULT_OK;
|
return dmExtension::RESULT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sticky input on the engine's window, once it exists (see the top).
|
||||||
|
static bool g_Sticky = false;
|
||||||
|
|
||||||
|
static dmExtension::Result Update(dmExtension::Params* params)
|
||||||
|
{
|
||||||
|
#if defined(MM_GLFW)
|
||||||
|
if (!g_Sticky) {
|
||||||
|
GLFWwindow* win = glfwGetCurrentContext();
|
||||||
|
if (win) {
|
||||||
|
glfwSetInputMode(win, MM_GLFW_STICKY_MOUSE_BUTTONS, 1);
|
||||||
|
glfwSetInputMode(win, MM_GLFW_STICKY_KEYS, 1);
|
||||||
|
g_Sticky = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return dmExtension::RESULT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
static dmExtension::Result Finalize(dmExtension::Params* params)
|
static dmExtension::Result Finalize(dmExtension::Params* params)
|
||||||
{
|
{
|
||||||
return dmExtension::RESULT_OK;
|
return dmExtension::RESULT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
DM_DECLARE_EXTENSION(EXTENSION_NAME, LIB_NAME, 0, 0, Initialize, 0, 0, Finalize)
|
DM_DECLARE_EXTENSION(EXTENSION_NAME, LIB_NAME, 0, 0, Initialize, Update, 0, Finalize)
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
-- (else the one holding the focused element, else the biggest one), like a browser.
|
-- (else the one holding the focused element, else the biggest one), like a browser.
|
||||||
-- F11 toggles fullscreen; Ctrl + / Ctrl - / Ctrl 0 change the UI scale (modules/screen),
|
-- F11 toggles fullscreen; Ctrl + / Ctrl - / Ctrl 0 change the UI scale (modules/screen),
|
||||||
-- like the browser's zoom. F3 (debug builds) shows the engine's profiler (frame times).
|
-- like the browser's zoom. F3 (debug builds) shows the engine's profiler (frame times).
|
||||||
|
-- G opens the quick grant dialog for admins (state.admin, screens/game/grant.lua).
|
||||||
--
|
--
|
||||||
-- Text fields made in a build register in g.inputs ({ input = Druid input, submit = fn,
|
-- Text fields made in a build register in g.inputs ({ input = Druid input, submit = fn,
|
||||||
-- step = { min, max } }: modules/field, the refinery's quantity); core/main resets the list
|
-- step = { min, max } }: modules/field, the refinery's quantity); core/main resets the list
|
||||||
@@ -32,6 +33,7 @@ local CTRL = { [hash("key_lctrl")] = true, [hash("key_rctrl")] = true }
|
|||||||
local F11 = hash("key_f11")
|
local F11 = hash("key_f11")
|
||||||
local F3 = hash("key_f3")
|
local F3 = hash("key_f3")
|
||||||
local ESC = hash("key_esc")
|
local ESC = hash("key_esc")
|
||||||
|
local KEY_G = hash("key_g")
|
||||||
local ZOOM = { [hash("key_plus")] = 1, [hash("key_equals")] = 1, [hash("key_numpad_add")] = 1,
|
local ZOOM = { [hash("key_plus")] = 1, [hash("key_equals")] = 1, [hash("key_numpad_add")] = 1,
|
||||||
[hash("key_minus")] = -1, [hash("key_numpad_subtract")] = -1, [hash("key_0")] = 0, [hash("key_numpad_0")] = 0 }
|
[hash("key_minus")] = -1, [hash("key_numpad_subtract")] = -1, [hash("key_0")] = 0, [hash("key_numpad_0")] = 0 }
|
||||||
local UP, DOWN = hash("key_up"), hash("key_down")
|
local UP, DOWN = hash("key_up"), hash("key_down")
|
||||||
@@ -243,6 +245,9 @@ function M.before(g, action_id, action, input)
|
|||||||
elseif action_id == MOUSE_FORWARD or (action_id == RIGHT and g.mods.alt) then
|
elseif action_id == MOUSE_FORWARD or (action_id == RIGHT and g.mods.alt) then
|
||||||
session.go(g, 1)
|
session.go(g, 1)
|
||||||
return true
|
return true
|
||||||
|
elseif action_id == KEY_G and not g.mods.ctrl and not g.mods.alt and g.state and g.state.admin then
|
||||||
|
require("screens.game.grant").open(g) -- (admins: the quick grant dialog)
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -352,7 +352,7 @@ function M.build(g)
|
|||||||
return banner.build(g, g.h, holder, W)
|
return banner.build(g, g.h, holder, W)
|
||||||
end)
|
end)
|
||||||
local header_h = blocks.keep(g, "header", blocks.sig(common, W, banner_h, g.view.section, st.credits, st.resources,
|
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)
|
st.unlocked, st.shine, st.admin, st.ready, st.nextEnd, ready.drills(g), ready.batches(g)), g.layout, function(holder)
|
||||||
g.tab_nodes = {}
|
g.tab_nodes = {}
|
||||||
local header_root = ui.rect(holder, 0, banner_h, W, 1, "none")
|
local header_root = ui.rect(holder, 0, banner_h, W, 1, "none")
|
||||||
g.header_node = header_root
|
g.header_node = header_root
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
-- Quick grant (admins only, state.admin): give yourself credits, a resource or a refined
|
||||||
|
-- item (server action admin-grant). Opened by the header's grant button or the G key
|
||||||
|
-- (core/keys). An item picker and an amount picker (buttons, the pick ringed), then Grant.
|
||||||
|
-- Test ids: "dialog:grant:<item>", "dialog:amount:<n>", "dialog:ok".
|
||||||
|
|
||||||
|
local ui = require("modules.ui")
|
||||||
|
local flex = require("modules.flex")
|
||||||
|
local fg = require("modules.flex_gui")
|
||||||
|
local theme = require("modules.theme")
|
||||||
|
|
||||||
|
local S = theme.size
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local AMOUNTS = { { 100, "100" }, { 1000, "1K" }, { 10000, "10K" }, { 100000, "100K" }, { 1000000, "1M" } }
|
||||||
|
local W = 520
|
||||||
|
|
||||||
|
-- The choices: credits, then the catalog's resources and refined items.
|
||||||
|
local function items(st)
|
||||||
|
local out = { { "credits", "Credits" } }
|
||||||
|
local cat = st.catalog or {}
|
||||||
|
for _, list in ipairs({ cat.resources or {}, cat.refined or {} }) do
|
||||||
|
for _, r in ipairs(list) do
|
||||||
|
table.insert(out, { r.name, r.name })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A wrapping row of small buttons (choices: { value, label }) at (x, y), inner width w.
|
||||||
|
local function picker(parent, choices, x, y, w)
|
||||||
|
local kids = {}
|
||||||
|
for i, c in ipairs(choices) do
|
||||||
|
kids[i] = { style = { width = fg.button_w(parent, c[2], { small = true }), height = S.button_sm_h, shrink = 0 } }
|
||||||
|
end
|
||||||
|
local tree = flex.box({ wrap = true, gap = 8, width = w }, kids)
|
||||||
|
flex.solve(tree, x, y)
|
||||||
|
return tree
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.open(g)
|
||||||
|
local st = g.state
|
||||||
|
if not (st and st.admin) or g.dialog:is_open() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local choices, amounts = items(st), {}
|
||||||
|
for i, a in ipairs(AMOUNTS) do
|
||||||
|
amounts[i] = { a[1], a[2] }
|
||||||
|
end
|
||||||
|
-- (laid out first: the dialog's height comes from it)
|
||||||
|
local x, y, iw = 24, 64, W - 48
|
||||||
|
local item_tree = picker(g.dialog.layer, choices, x, y + 22, iw)
|
||||||
|
local ay = y + 22 + item_tree.h + 20
|
||||||
|
local amount_tree = picker(g.dialog.layer, amounts, x, ay + 22, iw)
|
||||||
|
local h = ay + 22 + amount_tree.h + 24 + S.button_h + 24
|
||||||
|
local params = { grant = "credits", amount = "1000" }
|
||||||
|
g.dialog:open({
|
||||||
|
title = "Grant",
|
||||||
|
w = W,
|
||||||
|
h = h,
|
||||||
|
ok = { label = "Grant", act = "admin-grant", params = params },
|
||||||
|
build = function(d, box)
|
||||||
|
local rings = {}
|
||||||
|
local function pick(kind, tree, list, key, value_of)
|
||||||
|
for i, c in ipairs(list) do
|
||||||
|
local b = tree.children[i]
|
||||||
|
local function ring()
|
||||||
|
if rings[kind] then
|
||||||
|
gui.delete_node(rings[kind])
|
||||||
|
end
|
||||||
|
rings[kind] = ui.outline(box, b.x - 3, b.y - 3, b.w + 6, b.h + 6, "cyan")
|
||||||
|
end
|
||||||
|
d:button(box, c[2], b.x, b.y, { small = true, w = b.w, id = "dialog:" .. kind .. ":" .. tostring(c[1]),
|
||||||
|
on_click = function()
|
||||||
|
params[key] = value_of(c[1])
|
||||||
|
ring()
|
||||||
|
end })
|
||||||
|
if value_of(c[1]) == params[key] then
|
||||||
|
ring()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ui.text(box, "Item", "cap", x, y + 6)
|
||||||
|
pick("grant", item_tree, choices, "grant", tostring)
|
||||||
|
ui.text(box, "Amount", "cap", x, ay + 6)
|
||||||
|
pick("amount", amount_tree, amounts, "amount", function(n) return string.format("%d", n) end)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -134,6 +134,17 @@ local function wallet(g, parent, W)
|
|||||||
g.druid:new_button(disp, function()
|
g.druid:new_button(disp, function()
|
||||||
require("screens.game.settings").open(g)
|
require("screens.game.settings").open(g)
|
||||||
end):set_animations_disabled()
|
end):set_animations_disabled()
|
||||||
|
if st.admin then
|
||||||
|
-- Admins: the quick grant dialog (screens/game/grant.lua; G opens it too).
|
||||||
|
local gr = ui.outline(parent, 0, 0, 28, 28, "line_hi")
|
||||||
|
push(gr, 28, 28, 8)
|
||||||
|
ui.text(gr, "+", { font = "mono", px = 18, color = "cyan" }, 14, 14, gui.PIVOT_CENTER)
|
||||||
|
test.register("grant", gr, "button", "grant")
|
||||||
|
tooltip.add(gr, "Grant (G): credits, resources, refined items")
|
||||||
|
g.druid:new_button(gr, function()
|
||||||
|
require("screens.game.grant").open(g)
|
||||||
|
end):set_animations_disabled()
|
||||||
|
end
|
||||||
local tree = flex.solve({ style = { justify = "end", align = "center", padding_right = S.header_pad_x }, children = row }, 0, 16, W, 36)
|
local tree = flex.solve({ style = { justify = "end", align = "center", padding_right = S.header_pad_x }, children = row }, 0, 16, W, 36)
|
||||||
flex.apply(tree)
|
flex.apply(tree)
|
||||||
return row[1].x - 10
|
return row[1].x - 10
|
||||||
|
|||||||
+20
-5
@@ -236,7 +236,8 @@ class Game:
|
|||||||
self.window_origin()
|
self.window_origin()
|
||||||
if self.window_id:
|
if self.window_id:
|
||||||
self.xdo("windowfocus", "--sync", self.window_id)
|
self.xdo("windowfocus", "--sync", self.window_id)
|
||||||
# (the engine polls keys once a frame: a press and release between two frames is lost)
|
# (the engine polls keys once a frame; sticky keys (mmwindow) keep a press shorter
|
||||||
|
# than a frame, held like a hand anyway)
|
||||||
self.xdo("keydown", name)
|
self.xdo("keydown", name)
|
||||||
time.sleep(0.08)
|
time.sleep(0.08)
|
||||||
self.xdo("keyup", name)
|
self.xdo("keyup", name)
|
||||||
@@ -359,10 +360,24 @@ def main():
|
|||||||
if os.path.exists(disp):
|
if os.path.exists(disp):
|
||||||
os.environ["DISPLAY"] = open(disp).read().strip() # the headless make dev
|
os.environ["DISPLAY"] = open(disp).read().strip() # the headless make dev
|
||||||
elif not os.environ.get("DISPLAY_FOR_DRIVE"):
|
elif not os.environ.get("DISPLAY_FOR_DRIVE"):
|
||||||
# Re-run inside a fresh Xvfb display of the wanted size.
|
# Re-run inside a fresh Xvfb display of the wanted size. Xvfb picks a free display
|
||||||
cmd = ["xvfb-run", "-a", "-s", "-screen 0 %sx24" % args.size, sys.executable] + sys.argv
|
# itself (-displayfd): xvfb-run -a races when two runs start together, and both
|
||||||
env = dict(os.environ, DISPLAY_FOR_DRIVE="1")
|
# games then share one display (each run's clicks land on the other's window).
|
||||||
sys.exit(subprocess.call(cmd, env=env))
|
r, w = os.pipe()
|
||||||
|
xvfb = subprocess.Popen(["Xvfb", "-displayfd", str(w), "-screen", "0", "%sx24" % args.size, "-nolisten", "tcp"],
|
||||||
|
pass_fds=(w,), stderr=subprocess.DEVNULL)
|
||||||
|
os.close(w)
|
||||||
|
num = os.read(r, 32).decode().strip()
|
||||||
|
os.close(r)
|
||||||
|
if not num:
|
||||||
|
raise SystemExit("Xvfb didn't start")
|
||||||
|
env = dict(os.environ, DISPLAY_FOR_DRIVE="1", DISPLAY=":" + num)
|
||||||
|
try:
|
||||||
|
code = subprocess.call([sys.executable] + sys.argv, env=env)
|
||||||
|
finally:
|
||||||
|
xvfb.terminate()
|
||||||
|
xvfb.wait()
|
||||||
|
sys.exit(code)
|
||||||
|
|
||||||
path = find_scenario(args.scenario)
|
path = find_scenario(args.scenario)
|
||||||
name = os.path.splitext(os.path.basename(path))[0]
|
name = os.path.splitext(os.path.basename(path))[0]
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ def alive(game, what):
|
|||||||
target = next(t for t in TABS if t != section)
|
target = next(t for t in TABS if t != section)
|
||||||
b0 = game.eval("g.builds")
|
b0 = game.eval("g.builds")
|
||||||
n0 = game.eval("g.engine_inputs or 0")
|
n0 = game.eval("g.engine_inputs or 0")
|
||||||
|
game.eval("(function() g.il_touch = {} return true end)()")
|
||||||
ok = False
|
ok = False
|
||||||
game.os_click("tab:" + target) # (once: a rebuild between the press and the release carries it)
|
game.os_click("tab:" + target) # (once: a rebuild between the press and the release carries it)
|
||||||
for _ in range(15): # (the navigation started: with fake latency it lands seconds later)
|
for _ in range(15): # (the navigation started: with fake latency it lands seconds later)
|
||||||
@@ -36,6 +37,8 @@ def alive(game, what):
|
|||||||
break
|
break
|
||||||
game.sleep(0.1)
|
game.sleep(0.1)
|
||||||
got = game.eval("g.engine_inputs or 0") - n0
|
got = game.eval("g.engine_inputs or 0") - n0
|
||||||
|
if not ok: # (what the engine delivered: touch presses and releases, where, over what)
|
||||||
|
print(" touches:", game.eval("g.il_touch"))
|
||||||
print(" %s: %d engine inputs, builds %d before the click" % (what, got, b0))
|
print(" %s: %d engine inputs, builds %d before the click" % (what, got, b0))
|
||||||
game.expect(ok and got > 0, "%s: a real click reaches the page (%d inputs)" % (what, got))
|
game.expect(ok and got > 0, "%s: a real click reaches the page (%d inputs)" % (what, got))
|
||||||
game.idle()
|
game.idle()
|
||||||
@@ -81,6 +84,10 @@ def backdrop(game):
|
|||||||
|
|
||||||
def run(game, args):
|
def run(game, args):
|
||||||
fx = game.fixture
|
fx = game.fixture
|
||||||
|
game.eval("(function() local m=require('screens.game.core.main') if not m.il_i then m.il_i=m.on_input "
|
||||||
|
"m.on_input=function(g,a,x) if a==hash('touch') and (x.pressed or x.released) and g.il_touch then "
|
||||||
|
"table.insert(g.il_touch,{p=x.pressed and 1 or 0,x=x.x,y=x.y,b=g.builds,d=g.dialog.root and 1 or 0,"
|
||||||
|
"busy=g.busy and 1 or 0,f=socket.gettime()}) end return m.il_i(g,a,x) end end return true end)()")
|
||||||
if "reduced" in args:
|
if "reduced" in args:
|
||||||
game.set("reduced_motion", "1")
|
game.set("reduced_motion", "1")
|
||||||
game.idle()
|
game.idle()
|
||||||
|
|||||||
+2
-1
@@ -107,7 +107,8 @@ shared rules: `../AGENTS.md`. Run every command below from this directory (`serv
|
|||||||
sections and actions need a gate there.
|
sections and actions need a gate there.
|
||||||
- `players.go`, `web/templates/players.html`: admin Players page at `/utils/players`
|
- `players.go`, `web/templates/players.html`: admin Players page at `/utils/players`
|
||||||
(search, detail, grants, finish drills, reset, onboarding reset/unlock all, ban; each logged as `admin_*` in
|
(search, detail, grants, finish drills, reset, onboarding reset/unlock all, ban; each logged as `admin_*` in
|
||||||
`event_log`). `notBanned` gives banned players (`users.banned_at`) a suspended page.
|
`event_log`). In the game, admins (`Page.Admin`, `state.admin`) also get a quick grant
|
||||||
|
dialog (header Grant button or G; action `admin-grant`, 403 for others). `notBanned` gives banned players (`users.banned_at`) a suspended page.
|
||||||
`web/templates/admin.html`: the shared admin nav (`{{template "admin-nav" "<page>"}}`, put
|
`web/templates/admin.html`: the shared admin nav (`{{template "admin-nav" "<page>"}}`, put
|
||||||
it on every `/utils` page) and confirm dialog. `/utils` redirects to `/utils/players`.
|
it on every `/utils` page) and confirm dialog. `/utils` redirects to `/utils/players`.
|
||||||
- `web/templates/`: `page.html` (page shell, boot page), `app.html` (header, moon
|
- `web/templates/`: `page.html` (page shell, boot page), `app.html` (header, moon
|
||||||
|
|||||||
+7
-1
@@ -48,6 +48,7 @@ type APIState struct {
|
|||||||
Well *APIWell `json:"well"` // s=moons with a well selected
|
Well *APIWell `json:"well"` // s=moons with a well selected
|
||||||
Market *APIMarket `json:"market"` // once the market is unlocked
|
Market *APIMarket `json:"market"` // once the market is unlocked
|
||||||
Banner []APIBanner `json:"banner"`
|
Banner []APIBanner `json:"banner"`
|
||||||
|
Admin bool `json:"admin"` // may use admin-grant
|
||||||
|
|
||||||
// Always present (api_sections.go).
|
// Always present (api_sections.go).
|
||||||
ShowNav bool `json:"showNav"` // the section tabs show (not while Moons would be the only one)
|
ShowNav bool `json:"showNav"` // the section tabs show (not while Moons would be the only one)
|
||||||
@@ -380,7 +381,7 @@ func apiState(p *Page, extra ...*Page) *APIState {
|
|||||||
Credits: s.Balance, Resources: apiAmounts(s.Resources), Refined: []APIAmount{}, MoonPrice: s.MoonPrice(),
|
Credits: s.Balance, Resources: apiAmounts(s.Resources), Refined: []APIAmount{}, MoonPrice: s.MoonPrice(),
|
||||||
Unlocked: map[string]bool{}, Shine: []string{}, Ready: p.Ready, Idle: p.Idle, NextEnd: unixMS(p.NextEnd),
|
Unlocked: map[string]bool{}, Shine: []string{}, Ready: p.Ready, Idle: p.Idle, NextEnd: unixMS(p.NextEnd),
|
||||||
Moons: apiMoonList(p.Moons), Past: apiMoonList(p.Past), Tools: []APITool{}, Stakes: []APIStake{},
|
Moons: apiMoonList(p.Moons), Past: apiMoonList(p.Past), Tools: []APITool{}, Stakes: []APIStake{},
|
||||||
Banner: []APIBanner{}}
|
Banner: []APIBanner{}, Admin: p.Admin}
|
||||||
for _, r := range s.Refined {
|
for _, r := range s.Refined {
|
||||||
a.Refined = append(a.Refined, APIAmount{Name: r.Item, Amount: r.Balance})
|
a.Refined = append(a.Refined, APIAmount{Name: r.Item, Amount: r.Balance})
|
||||||
}
|
}
|
||||||
@@ -674,6 +675,7 @@ func (s *server) apiPageFor(ctx context.Context, state *State, v View) (*Page, e
|
|||||||
// catalog out, catalog=<catalogVersion> leaves it out while it's unchanged.
|
// catalog out, catalog=<catalogVersion> leaves it out while it's unchanged.
|
||||||
func (s *server) apiStateWith(ctx context.Context, page *Page, get func(string) string) (*APIState, error) {
|
func (s *server) apiStateWith(ctx context.Context, page *Page, get func(string) string) (*APIState, error) {
|
||||||
page.Banner = s.banner(ctx)
|
page.Banner = s.banner(ctx)
|
||||||
|
page.Admin = s.isAdmin(page.S.UserID)
|
||||||
var extra []*Page
|
var extra []*Page
|
||||||
for _, section := range apiIncludes(get("include")) {
|
for _, section := range apiIncludes(get("include")) {
|
||||||
if section == page.V.Section {
|
if section == page.V.Section {
|
||||||
@@ -765,6 +767,10 @@ func (s *server) handleAPIAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
apiError(w, http.StatusNotFound, "Unknown action")
|
apiError(w, http.StatusNotFound, "Unknown action")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, errForbidden) {
|
||||||
|
apiError(w, http.StatusForbidden, "Missing permission")
|
||||||
|
return
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiServerError(w, err)
|
apiServerError(w, err)
|
||||||
return
|
return
|
||||||
|
|||||||
+3
-1
@@ -80,6 +80,7 @@ The other sections' actions:
|
|||||||
| `dismiss-advisor` | `advisor` | frees their seat; nothing is refunded (confirm with `dismiss`) |
|
| `dismiss-advisor` | `advisor` | frees their seat; nothing is refunded (confirm with `dismiss`) |
|
||||||
| `sell-moon` | `sell-moon` (moon id) | `moon.sale.canSell`; view moves to another moon |
|
| `sell-moon` | `sell-moon` (moon id) | `moon.sale.canSell`; view moves to another moon |
|
||||||
| `new-game` | – | creates a new player and returns its id and state |
|
| `new-game` | – | creates a new player and returns its id and state |
|
||||||
|
| `admin-grant` | `grant` (`credits`, a resource or a refined item name), `amount` | admins only (`state.admin`), else `403`; logged as `admin_grant` |
|
||||||
|
|
||||||
Responses:
|
Responses:
|
||||||
- `200` `{ok: true, userId, clientSeq?, toast: {message, error: false, kind}, fx: {kind, finished, credits}, reveal?, buyModal?, state}`.
|
- `200` `{ok: true, userId, clientSeq?, toast: {message, error: false, kind}, fx: {kind, finished, credits}, reveal?, buyModal?, state}`.
|
||||||
@@ -88,7 +89,7 @@ Responses:
|
|||||||
(after `buy-tool`) are one-shot results, see below.
|
(after `buy-tool`) are one-shot results, see below.
|
||||||
- `422` rule violation or locked action: `{ok: false, error, toast: {message, error: true, kind: "error"}, state}`.
|
- `422` rule violation or locked action: `{ok: false, error, toast: {message, error: true, kind: "error"}, state}`.
|
||||||
A locked action (its part of the game not revealed yet) says "Not available yet".
|
A locked action (its part of the game not revealed yet) says "Not available yet".
|
||||||
- `400` bad body, `404` unknown action, `429` too many new players, `403` banned,
|
- `400` bad body, `404` unknown action, `429` too many new players, `403` banned or not an admin (`admin-grant`),
|
||||||
`500` `{ok: false, error: "Something went wrong"}`.
|
`500` `{ok: false, error: "Something went wrong"}`.
|
||||||
|
|
||||||
## GET /api/moon-settings
|
## GET /api/moon-settings
|
||||||
@@ -106,6 +107,7 @@ session; the web page also reads it once per page load.
|
|||||||
| `userId`, `now` | |
|
| `userId`, `now` | |
|
||||||
| `view` | `{section, moon, well, past, page, pick, drill, adv, aside}`, resolved (`page` clamped, `adv` only a hired advisor in `s=advisors`); `aside`: the web shows the aside (viewport and side panel): moon view, new-moon teaser, an open drill or advisor panel |
|
| `view` | `{section, moon, well, past, page, pick, drill, adv, aside}`, resolved (`page` clamped, `adv` only a hired advisor in `s=advisors`); `aside`: the web shows the aside (viewport and side panel): moon view, new-moon teaser, an open drill or advisor panel |
|
||||||
| `credits` | balance |
|
| `credits` | balance |
|
||||||
|
| `admin` | the player is an admin (`ADMIN_USER_IDS`, or everyone with `MM_DEV=1`): may use `admin-grant` |
|
||||||
| `resources`, `refined` | `[{name, amount}]` |
|
| `resources`, `refined` | `[{name, amount}]` |
|
||||||
| `moonPrice` | price of the next moon |
|
| `moonPrice` | price of the next moon |
|
||||||
| `unlocked` | `{moons, equipment, market, store, refinery, advisors, leaderboard, help, new-moon, claim-all, redrill, redeploy}`: booleans; hide what is false |
|
| `unlocked` | `{moons, equipment, market, store, refinery, advisors, leaderboard, help, new-moon, claim-all, redrill, redeploy}`: booleans; hide what is false |
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ func TestAPI(t *testing.T) {
|
|||||||
s.pagePastInMemory(v.Page)
|
s.pagePastInMemory(v.Page)
|
||||||
p := s.page(v.resolve(s))
|
p := s.page(v.resolve(s))
|
||||||
testRanking(p)
|
testRanking(p)
|
||||||
|
p.Admin = len(s.Tools) > 0
|
||||||
a := apiState(p)
|
a := apiState(p)
|
||||||
|
if a.Admin != p.Admin {
|
||||||
|
t.Errorf("view %+v: admin %v", v, a.Admin)
|
||||||
|
}
|
||||||
if _, err := json.Marshal(a); err != nil {
|
if _, err := json.Marshal(a); err != nil {
|
||||||
t.Fatalf("marshal %+v: %v", v, err)
|
t.Fatalf("marshal %+v: %v", v, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ func (s *server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
page.Banner = s.banner(r.Context())
|
page.Banner = s.banner(r.Context())
|
||||||
|
page.Admin = s.isAdmin(userID)
|
||||||
if htmx {
|
if htmx {
|
||||||
page.OOB = true
|
page.OOB = true
|
||||||
s.render(w, "regions", page)
|
s.render(w, "regions", page)
|
||||||
@@ -368,6 +369,9 @@ type actionResult struct {
|
|||||||
// errUnknownAction is runAction's error for an action that doesn't exist.
|
// errUnknownAction is runAction's error for an action that doesn't exist.
|
||||||
var errUnknownAction = errors.New("unknown action")
|
var errUnknownAction = errors.New("unknown action")
|
||||||
|
|
||||||
|
// errForbidden is runAction's error for an admin action run by a player who isn't one (403).
|
||||||
|
var errForbidden = errors.New("missing permission")
|
||||||
|
|
||||||
// runAction runs a game action: its onboarding gate, then its rule in game.go. get reads
|
// runAction runs a game action: its onboarding gate, then its rule in game.go. get reads
|
||||||
// the request's parameters, including the current view (s, moon, well, tab), which stays
|
// the request's parameters, including the current view (s, moon, well, tab), which stays
|
||||||
// unless the action moves on. new-game only passes the gate; the caller creates the player.
|
// unless the action moves on. new-game only passes the gate; the caller creates the player.
|
||||||
@@ -570,6 +574,20 @@ func (s *server) runAction(ctx context.Context, userID, action string, get func(
|
|||||||
if n, err = buyAdvisorSeat(ctx, s.db, userID); err == nil {
|
if n, err = buyAdvisorSeat(ctx, s.db, userID); err == nil {
|
||||||
message = fmt.Sprintf("Advisor seat %d added", n)
|
message = fmt.Sprintf("Advisor seat %d added", n)
|
||||||
}
|
}
|
||||||
|
case "admin-grant":
|
||||||
|
// The quick grant dialog: an admin gives themselves credits, a resource or a refined item.
|
||||||
|
if !s.isAdmin(userID) {
|
||||||
|
return actionResult{}, errForbidden
|
||||||
|
}
|
||||||
|
item := get("grant")
|
||||||
|
amount, _ := strconv.ParseFloat(get("amount"), 64)
|
||||||
|
if item == "credits" {
|
||||||
|
err = adminCredits(ctx, s.db, userID, userID, amount, false)
|
||||||
|
message = "Granted " + formatCredits(amount)
|
||||||
|
} else {
|
||||||
|
err = adminResources(ctx, s.db, userID, userID, item, amount)
|
||||||
|
message = fmt.Sprintf("Granted %s %s", formatMoney(amount), item)
|
||||||
|
}
|
||||||
case "new-game":
|
case "new-game":
|
||||||
default:
|
default:
|
||||||
return actionResult{}, errUnknownAction
|
return actionResult{}, errUnknownAction
|
||||||
@@ -601,6 +619,10 @@ func (s *server) handleAction(w http.ResponseWriter, r *http.Request, userID str
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, errForbidden) {
|
||||||
|
http.Error(w, "Missing permission", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serverError(w, err)
|
serverError(w, err)
|
||||||
return
|
return
|
||||||
@@ -632,6 +654,7 @@ func (s *server) handleAction(w http.ResponseWriter, r *http.Request, userID str
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
page.Banner = s.banner(ctx)
|
page.Banner = s.banner(ctx)
|
||||||
|
page.Admin = s.isAdmin(userID)
|
||||||
page.OOB = true
|
page.OOB = true
|
||||||
page.Toast = toast
|
page.Toast = toast
|
||||||
if res.BoughtTool != "" {
|
if res.BoughtTool != "" {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func TestTemplates(t *testing.T) {
|
|||||||
s.pagePastInMemory(v.Page)
|
s.pagePastInMemory(v.Page)
|
||||||
v = v.resolve(s)
|
v = v.resolve(s)
|
||||||
p := s.page(v)
|
p := s.page(v)
|
||||||
|
p.Admin = len(s.Tools) > 0 // the grant dialog and button, for some states
|
||||||
if v.Section == "leaderboard" {
|
if v.Section == "leaderboard" {
|
||||||
p.Ranking = &LeaderboardPage{Players: 12, Top: []LeaderboardRow{{Rank: 1, Name: "Top Miner", Balance: 9000}},
|
p.Ranking = &LeaderboardPage{Players: 12, Top: []LeaderboardRow{{Rank: 1, Name: "Top Miner", Balance: 9000}},
|
||||||
You: &LeaderboardRow{Rank: 12, Name: "Test Miner", Balance: 10, Me: true}}
|
You: &LeaderboardRow{Rank: 12, Name: "Test Miner", Balance: 10, Me: true}}
|
||||||
|
|||||||
@@ -171,6 +171,13 @@ POST :host/api/act/claim
|
|||||||
X-MM-User: :user_id
|
X-MM-User: :user_id
|
||||||
Content-Type: application/x-www-form-urlencoded
|
Content-Type: application/x-www-form-urlencoded
|
||||||
|
|
||||||
|
# Admins only (state.admin, else 403): grant yourself credits, a resource or a refined item
|
||||||
|
POST :host/api/act/admin-grant
|
||||||
|
X-MM-User: :user_id
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"grant": "credits", "amount": 1000}
|
||||||
|
|
||||||
# Sell resources (amount=<n> or all=1)
|
# Sell resources (amount=<n> or all=1)
|
||||||
POST :host/api/act/sell
|
POST :host/api/act/sell
|
||||||
X-MM-User: :user_id
|
X-MM-User: :user_id
|
||||||
|
|||||||
@@ -271,6 +271,15 @@ func (v View) ToDrill(id string) string {
|
|||||||
|
|
||||||
// Page is the data for the whole screen. Every htmx response re-renders all of it as
|
// Page is the data for the whole screen. Every htmx response re-renders all of it as
|
||||||
// out-of-band swaps (see web/templates/app.html).
|
// out-of-band swaps (see web/templates/app.html).
|
||||||
|
// RefinedNames lists every refined item, for the grant dialog.
|
||||||
|
func (p *Page) RefinedNames() []string {
|
||||||
|
var names []string
|
||||||
|
for _, item := range cfg().Refinery.Items {
|
||||||
|
names = append(names, item.Name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
type Page struct {
|
type Page struct {
|
||||||
S *State
|
S *State
|
||||||
V View
|
V View
|
||||||
@@ -280,6 +289,7 @@ type Page struct {
|
|||||||
Modal *BuyModal // opens after buying a tool
|
Modal *BuyModal // opens after buying a tool
|
||||||
|
|
||||||
Banner []BannerItem // active announcements and live events (events.go)
|
Banner []BannerItem // active announcements and live events (events.go)
|
||||||
|
Admin bool // the player is an admin: the quick grant dialog (admin-grant)
|
||||||
|
|
||||||
Ready int // finished stakes, all moons
|
Ready int // finished stakes, all moons
|
||||||
NextEnd time.Time // when the soonest unfinished stake finishes (zero if none)
|
NextEnd time.Time // when the soonest unfinished stake finishes (zero if none)
|
||||||
|
|||||||
@@ -2101,6 +2101,15 @@ a.adv-seat-in[aria-current] {
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grant-fields {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.grant-fields .input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-actions {
|
.dialog-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -134,6 +134,34 @@ const dialogSound = (value) => sfx.play(value === "ok" ? "confirm" : "close");
|
|||||||
wireDialog(dialog, { onClose: dialogSound });
|
wireDialog(dialog, { onClose: dialogSound });
|
||||||
confirmWith(dialog, { onOpen: () => sfx.play("open") });
|
confirmWith(dialog, { onOpen: () => sfx.play("open") });
|
||||||
|
|
||||||
|
// Admins: the quick grant dialog (#grant), from the header's Grant button or the G key.
|
||||||
|
const grant = document.getElementById("grant");
|
||||||
|
if (grant) {
|
||||||
|
const openGrant = () => {
|
||||||
|
if (document.querySelector("dialog[open]")) return;
|
||||||
|
grant.showModal();
|
||||||
|
sfx.play("open");
|
||||||
|
};
|
||||||
|
wireDialog(grant, {
|
||||||
|
onClose: (value) => {
|
||||||
|
dialogSound(value);
|
||||||
|
if (value !== "ok") return;
|
||||||
|
const f = grant.querySelector("form");
|
||||||
|
htmx.ajax("POST", "/act/admin-grant", { source: f, swap: "none",
|
||||||
|
values: { grant: f.elements.grant.value, amount: f.elements.amount.value } });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
grant.querySelector("button[value=cancel]").addEventListener("click", () => closeDialog(grant, "cancel"));
|
||||||
|
document.addEventListener("click", (event) => {
|
||||||
|
if (event.target.closest("[data-grant-open]")) openGrant();
|
||||||
|
});
|
||||||
|
addEventListener("keydown", (event) => {
|
||||||
|
if (event.key !== "g" && event.key !== "G") return;
|
||||||
|
if (event.ctrlKey || event.metaKey || event.altKey || event.target.closest?.("input, select, textarea, [contenteditable]")) return;
|
||||||
|
openGrant();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Instant feedback: everything is disabled while requests run (CSS: .is-busy).
|
// Instant feedback: everything is disabled while requests run (CSS: .is-busy).
|
||||||
let inFlight = 0;
|
let inFlight = 0;
|
||||||
document.body.addEventListener("htmx:beforeRequest", () => {
|
document.body.addEventListener("htmx:beforeRequest", () => {
|
||||||
|
|||||||
@@ -127,10 +127,33 @@
|
|||||||
{{template "theme-toggle"}}
|
{{template "theme-toggle"}}
|
||||||
<!-- TODO: show only to admins once accounts/auth exist -->
|
<!-- TODO: show only to admins once accounts/auth exist -->
|
||||||
<a href="/utils" class="top-btn">Admin</a>
|
<a href="/utils" class="top-btn">Admin</a>
|
||||||
|
{{if .Admin}}<button type="button" class="top-btn" data-grant-open title="Grant yourself anything (G)">Grant</button>{{end}}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{/* Admins only: give yourself credits, a resource or a refined item (admin-grant). Opened
|
||||||
|
by the header's Grant button or the G key; main.js posts it when Grant closes it. */}}
|
||||||
|
{{define "grant-dialog"}}
|
||||||
|
<dialog id="grant" class="dialog frame" aria-labelledby="grant-title">
|
||||||
|
<form method="dialog">
|
||||||
|
<h2 id="grant-title" class="dialog-title">Grant</h2>
|
||||||
|
<div class="dialog-message grant-fields">
|
||||||
|
<select name="grant" class="input" aria-label="Item">
|
||||||
|
<option value="credits">₥ Credits</option>
|
||||||
|
{{range .S.Config.Resources}}<option>{{.}}</option>{{end}}
|
||||||
|
{{range .RefinedNames}}<option>{{.}}</option>{{end}}
|
||||||
|
</select>
|
||||||
|
<input name="amount" class="input num" type="number" min="0.01" step="any" value="1000" required aria-label="Amount">
|
||||||
|
</div>
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button type="button" value="cancel" class="btn">Cancel</button>
|
||||||
|
<button value="ok" class="btn btn-primary">Grant</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{/* Claims every finished drill (the tools go back to the inventory). Shown while anything
|
{{/* Claims every finished drill (the tools go back to the inventory). Shown while anything
|
||||||
is mining; disabled with a countdown until the first drill finishes (main.js enables
|
is mining; disabled with a countdown until the first drill finishes (main.js enables
|
||||||
it then and keeps the ready count current; the server decides what is actually ready). */}}
|
it then and keeps the ready count current; the server decides what is actually ready). */}}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
<div id="toasts" class="toasts" aria-live="polite" hx-ext="ignore:morph"></div>
|
<div id="toasts" class="toasts" aria-live="polite" hx-ext="ignore:morph"></div>
|
||||||
<div id="splash" class="splash" hidden></div>
|
<div id="splash" class="splash" hidden></div>
|
||||||
{{template "confirm-dialog"}}
|
{{template "confirm-dialog"}}
|
||||||
|
{{if .Admin}}{{template "grant-dialog" .}}{{end}}
|
||||||
<div id="modal"></div>
|
<div id="modal"></div>
|
||||||
<div id="reveal" hidden></div>
|
<div id="reveal" hidden></div>
|
||||||
{{template "view" .}}
|
{{template "view" .}}
|
||||||
|
|||||||
Reference in new issue
Block a user