Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cad48d8856 | ||
|
|
7da4ab8103 | ||
|
|
80cb1ffb07 | ||
|
|
9b61acd61a | ||
|
|
b0bc8135bb | ||
|
|
1510bf10a3 | ||
|
|
75f1820e83 | ||
|
|
8f57dedfe6 | ||
|
|
afb4dc4cca | ||
|
|
956d6c506e |
No files matched your search
+4
-2
@@ -90,11 +90,13 @@ run: build
|
|||||||
|
|
||||||
# Screenshot of the running game at 1280x800 under Xvfb (POPUP=1 opens the modal too).
|
# Screenshot of the running game at 1280x800 under Xvfb (POPUP=1 opens the modal too).
|
||||||
shot: build
|
shot: build
|
||||||
scripts/shot.sh $(BIN) $(SHOT) $(ARGS) $(if $(POPUP),--config=mm.popup=1)
|
scripts/shot.sh $(BIN) $(SHOT) $(if $(MM_API),--config=mm.api_base=$(MM_API)) $(ARGS) $(if $(POPUP),--config=mm.popup=1)
|
||||||
|
|
||||||
# Scripted play-through as a new player (mm.autoplay), a screenshot per stage in dist/play/.
|
# Scripted play-through as a new player (mm.autoplay), a screenshot per stage in dist/play/.
|
||||||
|
# Needs MM_API (a test server): it makes a new player there.
|
||||||
play: build
|
play: build
|
||||||
SHOTS_DIR=$(abspath dist/play) scripts/shot.sh $(BIN) dist/play/last.png --config=mm.autoplay=1 --config=mm.new_player=1 $(ARGS)
|
@test -n "$(MM_API)" || { echo "make play: set MM_API to a test server (no default)"; exit 1; }
|
||||||
|
SHOTS_DIR=$(abspath dist/play) scripts/shot.sh $(BIN) dist/play/last.png --config=mm.api_base=$(MM_API) --config=mm.autoplay=1 --config=mm.new_player=1 $(ARGS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf build dist .internal/cache
|
rm -rf build dist .internal/cache
|
||||||
+19
-1
@@ -1015,9 +1015,27 @@ function M:batch_ready(g, jobs)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- (a text node's position is its pivot point and its size not its text's: the box is the
|
||||||
|
-- text's own, round that point)
|
||||||
|
local PIVOT = { -- (fraction of the box left of / above the pivot point)
|
||||||
|
[gui.PIVOT_NW] = { 0, 0 }, [gui.PIVOT_N] = { 0.5, 0 }, [gui.PIVOT_NE] = { 1, 0 },
|
||||||
|
[gui.PIVOT_W] = { 0, 0.5 }, [gui.PIVOT_CENTER] = { 0.5, 0.5 }, [gui.PIVOT_E] = { 1, 0.5 },
|
||||||
|
[gui.PIVOT_SW] = { 0, 1 }, [gui.PIVOT_S] = { 0.5, 1 }, [gui.PIVOT_SE] = { 1, 1 },
|
||||||
|
}
|
||||||
|
|
||||||
function M:box_of(key)
|
function M:box_of(key)
|
||||||
local b = self.binds[key]
|
local b = self.binds[key]
|
||||||
return b and self:rect_of(b.box or b.node)
|
if not b then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
if b.box or gui.get_type(b.node) ~= gui.TYPE_TEXT then
|
||||||
|
return self:rect_of(b.box or b.node)
|
||||||
|
end
|
||||||
|
local r = self:rect_of(b.node)
|
||||||
|
local k = PIVOT[gui.get_pivot(b.node)] or { 0, 0 }
|
||||||
|
local w = ui.text_width(b.node)
|
||||||
|
local h = r.h * gui.get_scale(b.node).y
|
||||||
|
return { x = r.x - k[1] * w, y = r.y - k[2] * h, w = w, h = h }
|
||||||
end
|
end
|
||||||
|
|
||||||
function M:play(g, k, data, fxd, gains, credits, at, revealing, newly_ready)
|
function M:play(g, k, data, fxd, gains, credits, at, revealing, newly_ready)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ local M = {}
|
|||||||
M.id = nil
|
M.id = nil
|
||||||
M.rect = nil -- { x, y, w, h } window pixels, origin bottom-left
|
M.rect = nil -- { x, y, w, h } window pixels, origin bottom-left
|
||||||
M.unit = 1 -- window pixels per GUI reference pixel (the web's CSS pixel)
|
M.unit = 1 -- window pixels per GUI reference pixel (the web's CSS pixel)
|
||||||
|
M.covered = false -- a dialog is open over the page: the viewport takes no drag or wheel
|
||||||
|
|
||||||
-- Settings: nil until loaded (or failed: then the defaults). Replace the table to change
|
-- Settings: nil until loaded (or failed: then the defaults). Replace the table to change
|
||||||
-- them (moon.script compares by identity; the moon lab does, and Save sets the saved ones).
|
-- them (moon.script compares by identity; the moon lab does, and Save sets the saved ones).
|
||||||
|
|||||||
+35
-16
@@ -582,17 +582,17 @@ end
|
|||||||
-- Without redeploy the claimed tools are idle tools that last mined their well: its Redrill
|
-- Without redeploy the claimed tools are idle tools that last mined their well: its Redrill
|
||||||
-- count goes up (the server's LastWell).
|
-- count goes up (the server's LastWell).
|
||||||
M.register("claim", function(st, p, ctx)
|
M.register("claim", function(st, p, ctx)
|
||||||
if p["claim-wide"] then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
local list = {}
|
local list = {}
|
||||||
for _, s in ipairs(st.stakes) do
|
for _, s in ipairs(st.stakes) do
|
||||||
local wide = (s.reach or 1) > 1
|
local wide = (s.reach or 1) > 1
|
||||||
if done(s, ctx) and (not p["claim-well"] or (s.wellId == p["claim-well"] and not wide))
|
local scope
|
||||||
and (not p["claim-moon"] or s.moonId == p["claim-moon"]) then
|
if p["claim-wide"] then
|
||||||
if wide then
|
scope = wide and s.moonId == p["claim-wide"]
|
||||||
return false -- multi-well drills: the server splits them
|
else
|
||||||
end
|
scope = (not p["claim-well"] or (s.wellId == p["claim-well"] and not wide))
|
||||||
|
and (not p["claim-moon"] or s.moonId == p["claim-moon"])
|
||||||
|
end
|
||||||
|
if done(s, ctx) and scope then
|
||||||
list[#list + 1] = s
|
list[#list + 1] = s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -600,14 +600,33 @@ M.register("claim", function(st, p, ctx)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
-- Oldest first, each from what the earlier claims left (claimInTx: at most the supply).
|
-- Oldest first, each from what the earlier claims left (claimInTx: at most the supply).
|
||||||
|
-- A multi-well drill takes from its well, then the moon's next ones (split).
|
||||||
table.sort(list, function(a, b) return a.start < b.start end)
|
table.sort(list, function(a, b) return a.start < b.start end)
|
||||||
local emptied = {}
|
local emptied = {}
|
||||||
for _, s in ipairs(list) do
|
for _, s in ipairs(list) do
|
||||||
local take = math.max(0, math.min(s.amount or s.take or 0, M.supply(st, s.wellId)))
|
local moon = find(st.moons, s.moonId)
|
||||||
M.add_item(st, s.resource, take * M.skew)
|
if (s.reach or 1) > 1 and moon then
|
||||||
M.add_supply(st, s.wellId, -take)
|
local sup = {}
|
||||||
if take > 0 and M.supply(st, s.wellId) <= 0 then
|
for i, w in ipairs(moon.wells or {}) do
|
||||||
emptied[s.wellId] = true
|
sup[i] = M.supply(st, w.id)
|
||||||
|
end
|
||||||
|
for i, take in ipairs(split(moon, s, sup)) do
|
||||||
|
local w = moon.wells[i]
|
||||||
|
if take > 0 then
|
||||||
|
M.add_item(st, w.resource or s.resource, take * M.skew)
|
||||||
|
M.add_supply(st, w.id, -take)
|
||||||
|
if M.supply(st, w.id) <= 0 then
|
||||||
|
emptied[w.id] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local take = math.max(0, math.min(s.amount or s.take or 0, M.supply(st, s.wellId)))
|
||||||
|
M.add_item(st, s.resource, take * M.skew)
|
||||||
|
M.add_supply(st, s.wellId, -take)
|
||||||
|
if take > 0 and M.supply(st, s.wellId) <= 0 then
|
||||||
|
emptied[s.wellId] = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
M.remove_stake(st, s) -- its tool is idle; Redrill there counts it
|
M.remove_stake(st, s) -- its tool is idle; Redrill there counts it
|
||||||
end
|
end
|
||||||
@@ -633,7 +652,7 @@ M.register("claim", function(st, p, ctx)
|
|||||||
end
|
end
|
||||||
local amount = tool and w and (group and group.yield or tool.yield * yield_boost(st, w.resource))
|
local amount = tool and w and (group and group.yield or tool.yield * yield_boost(st, w.resource))
|
||||||
-- Not on a drained or fully committed well (the server leaves the tool idle).
|
-- Not on a drained or fully committed well (the server leaves the tool idle).
|
||||||
if tool and not tool.stakeId and w and w.supply > 0 and (M.room(st, w.id, amount, 1) or 1) > EPS then
|
if tool and not tool.stakeId and w and w.supply > 0 and (M.room(st, w.id, amount, tool.reach or 1) or 1) > EPS then
|
||||||
M.add_stake(st, tool, s.moonId, w, ctx, group)
|
M.add_stake(st, tool, s.moonId, w, ctx, group)
|
||||||
local detail = st.moon and find(st.moon.wells, s.wellId)
|
local detail = st.moon and find(st.moon.wells, s.wellId)
|
||||||
if detail and M.memo.last_well[tool.id] ~= s.wellId and (detail.redrill or 0) > 0 then
|
if detail and M.memo.last_well[tool.id] ~= s.wellId and (detail.redrill or 0) > 0 then
|
||||||
@@ -664,7 +683,7 @@ M.register("stake", function(st, p, ctx)
|
|||||||
return false -- "Deploy there": the view moves to the other well, wait for it
|
return false -- "Deploy there": the view moves to the other well, wait for it
|
||||||
end
|
end
|
||||||
local tool = M.idle_copy(st, p.tool)
|
local tool = M.idle_copy(st, p.tool)
|
||||||
if not tool or (tool.reach or 1) > 1 then
|
if not tool then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local group = nil
|
local group = nil
|
||||||
@@ -677,7 +696,7 @@ M.register("stake", function(st, p, ctx)
|
|||||||
end
|
end
|
||||||
-- Refused when the drills on the well take all it has left (fullyCommitted).
|
-- Refused when the drills on the well take all it has left (fullyCommitted).
|
||||||
local amount = group and group.yield or tool.yield * yield_boost(st, w.resource)
|
local amount = group and group.yield or tool.yield * yield_boost(st, w.resource)
|
||||||
if (M.room(st, w.id, amount, 1) or 0) <= EPS then
|
if (M.room(st, w.id, amount, tool.reach or 1) or 0) <= EPS then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
M.add_stake(st, tool, moon.id, w, ctx, group)
|
M.add_stake(st, tool, moon.id, w, ctx, group)
|
||||||
|
|||||||
@@ -605,7 +605,7 @@ function update(self, dt)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function on_input(self, action_id, action)
|
function on_input(self, action_id, action)
|
||||||
local rect = moon.frame.visible and moon.rect
|
local rect = moon.frame.visible and not moon.covered and moon.rect
|
||||||
if not rect then
|
if not rect then
|
||||||
self.dragging = false
|
self.dragging = false
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -435,7 +435,13 @@ function M.build(g, parent, x, y, w, h, H, mod, row)
|
|||||||
local section = g.view.section
|
local section = g.view.section
|
||||||
local moon = section == "moons" and st.moon or nil
|
local moon = section == "moons" and st.moon or nil
|
||||||
local cy
|
local cy
|
||||||
if row then
|
-- A section panel that wants the whole aside (the drill panel): no moon over it.
|
||||||
|
local full = mod and mod.aside_full and st[section] and mod.aside_full(g)
|
||||||
|
if full then
|
||||||
|
moon_view.hide()
|
||||||
|
g.viewport_node, g.viewport_rect = nil, nil
|
||||||
|
cy = 8
|
||||||
|
elseif row then
|
||||||
local vp = M.row_viewport(w)
|
local vp = M.row_viewport(w)
|
||||||
viewport(g, root, pad, pad, vp, shown_moon(g))
|
viewport(g, root, pad, pad, vp, shown_moon(g))
|
||||||
-- The side panel gets the rest of the row as its own box.
|
-- The side panel gets the rest of the row as its own box.
|
||||||
|
|||||||
@@ -670,6 +670,8 @@ function M.update(g, dt)
|
|||||||
g.fx:update(dt)
|
g.fx:update(dt)
|
||||||
g.reveal:update(dt)
|
g.reveal:update(dt)
|
||||||
g.dialog:update(dt)
|
g.dialog:update(dt)
|
||||||
|
-- (moon.script takes input before the GUI: a menu over the viewport must get its clicks)
|
||||||
|
moon_view.covered = g.dialog:is_open()
|
||||||
cards.ask_update(g)
|
cards.ask_update(g)
|
||||||
check_input(g)
|
check_input(g)
|
||||||
tooltip.update(g.dialog:is_open())
|
tooltip.update(g.dialog:is_open())
|
||||||
|
|||||||
@@ -167,8 +167,7 @@ local function geometry(inner)
|
|||||||
return {
|
return {
|
||||||
wide = wide,
|
wide = wide,
|
||||||
gap = wide and 12 or 10,
|
gap = wide and 12 or 10,
|
||||||
media_w = wide and 48 or 40, media_h = wide and 40 or 34,
|
status = wide and math.min(200, math.floor(inner * 0.16)) or 0,
|
||||||
status = wide and math.min(230, math.floor(inner * 0.22)) or 0,
|
|
||||||
track = wide and 124 or 90,
|
track = wide and 124 or 90,
|
||||||
sell = wide and 150 or 60,
|
sell = wide and 150 or 60,
|
||||||
name_px = wide and 14 or 13,
|
name_px = wide and 14 or 13,
|
||||||
@@ -382,14 +381,17 @@ local function tool_row(self, H, c, st, t, copy, geo, w, open, conf)
|
|||||||
name.done = function(n)
|
name.done = function(n)
|
||||||
tooltip.add(n, t.name .. ": yield level " .. (t.yieldLevel or 0) .. ", cycle level " .. (t.cycleLevel or 0))
|
tooltip.add(n, t.name .. ": yield level " .. (t.yieldLevel or 0) .. ", cycle level " .. (t.cycleLevel or 0))
|
||||||
end
|
end
|
||||||
|
local rh = row_h(open, wide)
|
||||||
|
-- The picture, square, takes the row's whole height left of every line (narrow: at most 64).
|
||||||
|
local mh = wide and rh - PADY * 2 or math.min(rh - PADY * 2, 64)
|
||||||
|
local mw = mh
|
||||||
|
local media = made({ width = mw, height = mh, shrink = 0 }, function(p, x, y)
|
||||||
|
local m = ui.rect(p, x, y, mw, mh, "bg")
|
||||||
|
ui.outline(m, 0, 0, mw, mh, "line")
|
||||||
|
cards.image(m, t.image, (mw - mh) / 2, 0, mh)
|
||||||
|
end)
|
||||||
local lines = {
|
local lines = {
|
||||||
row({ height = LINE1, gap = gap, align = "center" }, {
|
row({ height = LINE1, gap = gap, align = "center" }, {
|
||||||
made({ width = geo.media_w, height = geo.media_h, shrink = 0 }, function(p, x, y)
|
|
||||||
local mw, mh = geo.media_w, geo.media_h
|
|
||||||
local m = ui.rect(p, x, y, mw, mh, "bg")
|
|
||||||
ui.outline(m, 0, 0, mw, mh, "line")
|
|
||||||
cards.image(m, t.image, (mw - mh) / 2, 0, mh)
|
|
||||||
end),
|
|
||||||
col({ grow = 1, shrink = 1, basis = 0 }, {
|
col({ grow = 1, shrink = 1, basis = 0 }, {
|
||||||
name,
|
name,
|
||||||
fg.label(c, tagline, { font = "mono", px = 12, color = "muted" }, 16, { fit = true }),
|
fg.label(c, tagline, { font = "mono", px = 12, color = "muted" }, 16, { fit = true }),
|
||||||
@@ -443,9 +445,10 @@ local function tool_row(self, H, c, st, t, copy, geo, w, open, conf)
|
|||||||
lines[#lines + 1] = space(0, 6)
|
lines[#lines + 1] = space(0, 6)
|
||||||
lines[#lines + 1] = row({ height = 20, align = "center" }, { left })
|
lines[#lines + 1] = row({ height = 20, align = "center" }, { left })
|
||||||
end
|
end
|
||||||
local rh = row_h(open, wide)
|
|
||||||
local selected = t.selected and self.view.drill ~= ""
|
local selected = t.selected and self.view.drill ~= ""
|
||||||
return col({ width = w, height = rh, padding = { PADY, PADX, PADY, PADX } }, lines, { make = function(p, x, y, bw, bh)
|
return row({ width = w, height = rh, gap = gap, padding = { PADY, PADX, PADY, PADX } }, {
|
||||||
|
media, col({ grow = 1, shrink = 1, basis = 0 }, lines),
|
||||||
|
}, { make = function(p, x, y, bw, bh)
|
||||||
local slot = ui.rect(p, x, y, bw, bh, "none")
|
local slot = ui.rect(p, x, y, bw, bh, "none")
|
||||||
local body = ui.rect(slot, 0, 0, bw, bh, "cyan", 0)
|
local body = ui.rect(slot, 0, 0, bw, bh, "cyan", 0)
|
||||||
row_c.bind(self, body, bw, bh, { selected = selected, id = "drill:" .. t.id, label = t.name, on_click = function()
|
row_c.bind(self, body, bw, bh, { selected = selected, id = "drill:" .. t.id, label = t.name, on_click = function()
|
||||||
@@ -576,6 +579,11 @@ local function ladder(st, c, tr, inner)
|
|||||||
return col({}, { box({}, { head, rate_head }), col({ gap = 1 }, rows), space(0, 11) })
|
return col({}, { box({}, { head, rate_head }), col({ gap = 1 }, rows), space(0, 11) })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The drill panel takes the whole aside (core/aside.lua: no moon over it).
|
||||||
|
function M.aside_full(self)
|
||||||
|
return self.state.equipment.drill ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
function M.aside(self, root, w, h, H)
|
function M.aside(self, root, w, h, H)
|
||||||
local st = self.state
|
local st = self.state
|
||||||
local d = st.equipment and st.equipment.drill
|
local d = st.equipment and st.equipment.drill
|
||||||
|
|||||||
@@ -147,6 +147,25 @@ function M.build_teaser(g, root, px, py, inner)
|
|||||||
ui.rect(p, x + 16, y, w - 32, 1, "edge")
|
ui.rect(p, x + 16, y, w - 32, 1, "edge")
|
||||||
return nil
|
return nil
|
||||||
end } or nil)
|
end } or nil)
|
||||||
|
-- Moon packs (once the store is open): several at once, cheaper and better, a button
|
||||||
|
-- each under the grade's facts.
|
||||||
|
if #(e.packs or {}) > 0 then
|
||||||
|
local packs = {}
|
||||||
|
for _, pk in ipairs(e.packs) do
|
||||||
|
local label = pk.count .. " moons · " .. theme.currency .. ui.money(pk.price)
|
||||||
|
packs[#packs + 1] = fg.button(root, label, { small = true, disabled = not pk.canAfford }, function(b, nodes)
|
||||||
|
local save = math.floor(pk.discount * 100 + 0.5)
|
||||||
|
tooltip.add(b, pk.canAfford and (pk.count .. " " .. e.grade .. " moons for " .. theme.currency .. ui.money(pk.price)
|
||||||
|
.. " (" .. save .. "% off), each a little richer than a single one")
|
||||||
|
or ("Need " .. theme.currency .. ui.money(pk.price - st.credits) .. " more"))
|
||||||
|
if pk.canAfford then
|
||||||
|
g.h.action(g, b, nodes, "buy-moon", { grade = e.grade, count = pk.count, include = "new-moon" },
|
||||||
|
function() g.nm_grade = nil end, nil, "newmoon:pack:" .. e.grade .. ":" .. pk.count)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
rows[#rows + 1] = row({ height = 40, align = "start", padding = { 0, 16, 10, 56 }, gap = 8 }, packs)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
local picker = panel_box({ width = inner, padding = { 14, 0, 6, 0 } }, {
|
local picker = panel_box({ width = inner, padding = { 14, 0, 6, 0 } }, {
|
||||||
row({ padding = { 0, 16, 0, 16 } }, { fg.label(root, "Moon grades", "cap", 12) }),
|
row({ padding = { 0, 16, 0, 16 } }, { fg.label(root, "Moon grades", "cap", 12) }),
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""The settings menu's rows take real mouse clicks (xdotool), also where the menu lies over
|
||||||
|
the moon viewport (its drag used to take the press)."""
|
||||||
|
|
||||||
|
|
||||||
|
def run(game, args):
|
||||||
|
game.idle()
|
||||||
|
checks = [
|
||||||
|
("mute", "tostring(require('modules.sfx').muted())"),
|
||||||
|
("theme", "tostring(require('modules.theme').light)"),
|
||||||
|
("motion", "tostring(require('modules.anim').reduced)"),
|
||||||
|
("dialog:fullscreen", "tostring(mmwindow.is_fullscreen())"),
|
||||||
|
("menu:save-code", "tostring(g.dialog:is_open())"),
|
||||||
|
]
|
||||||
|
for id_, expr in checks:
|
||||||
|
game.os_click("settings")
|
||||||
|
game.wait_for("g.dialog:is_open()", timeout=5)
|
||||||
|
game.sleep(0.8)
|
||||||
|
before = game.eval(expr)
|
||||||
|
game.os_click(id_)
|
||||||
|
game.sleep(1.2)
|
||||||
|
after = game.eval(expr)
|
||||||
|
print(" %-18s %s -> %s dialog open: %s" % (id_, before, after, game.eval("tostring(g.dialog:is_open())")))
|
||||||
|
game.shot("after-" + id_.replace(":", "_"), settle=0.2)
|
||||||
|
game.key("key_esc")
|
||||||
|
game.sleep(0.6)
|
||||||
@@ -17,6 +17,10 @@ if [ -z "${MM_IN_XVFB:-}" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
export LIBGL_ALWAYS_SOFTWARE=${LIBGL_ALWAYS_SOFTWARE:-1}
|
export LIBGL_ALWAYS_SOFTWARE=${LIBGL_ALWAYS_SOFTWARE:-1}
|
||||||
|
# The game's saved files (player ids, settings) go beside the screenshot, never into the
|
||||||
|
# player's own (~/.local/share/moonminers): mm.new_player=1 would clear their saved id.
|
||||||
|
export XDG_DATA_HOME=${MM_SHOT_DATA:-$(dirname "$out")/data}
|
||||||
|
mkdir -p "$XDG_DATA_HOME"
|
||||||
cd "$(dirname "$bin")"
|
cd "$(dirname "$bin")"
|
||||||
log=$(dirname "$out")/shot.log
|
log=$(dirname "$out")/shot.log
|
||||||
TIMEOUT=${TIMEOUT:-120}
|
TIMEOUT=${TIMEOUT:-120}
|
||||||
|
|||||||
+1
-1
@@ -98,7 +98,7 @@ every command below from this directory (`server/`).
|
|||||||
Bonuses are fixed when the action runs (a stake keeps its duration and amount).
|
Bonuses are fixed when the action runs (a stake keeps its duration and amount).
|
||||||
- `tracks.go`: drill upgrade tracks. Each store item has two named tracks (`tracks.yield`,
|
- `tracks.go`: drill upgrade tracks. Each store item has two named tracks (`tracks.yield`,
|
||||||
`tracks.cycle` in game.json): levels that set the claim per cycle or the cycle in seconds
|
`tracks.cycle` in game.json): levels that set the claim per cycle or the cycle in seconds
|
||||||
flat, with a price and, higher up, refined `parts`. Levels are per tool
|
flat, with a price and optional refined `parts` (none in game.json now: credits only). Levels are per tool
|
||||||
(`inventory_item.yield_level`, `cycle_level`; copies with the same levels are
|
(`inventory_item.yield_level`, `cycle_level`; copies with the same levels are
|
||||||
interchangeable, `pickTool`); action `upgrade` (`track=yield|cycle`), counted from the next
|
interchangeable, `pickTool`); action `upgrade` (`track=yield|cycle`), counted from the next
|
||||||
stake. Reach is the item's and never changes. Also the view models (`TrackRow` with its
|
stake. Reach is the item's and never changes. Also the view models (`TrackRow` with its
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ game is moon and drill prices with wells scaled alike.
|
|||||||
|
|
||||||
Attachments and the T1–T5 tier ladder are gone. Each drill has two tracks of its own
|
Attachments and the T1–T5 tier ladder are gone. Each drill has two tracks of its own
|
||||||
(config `storeItems[].tracks`), 4–5 levels each, every level a flat value: yield (claim per
|
(config `storeItems[].tracks`), 4–5 levels each, every level a flat value: yield (claim per
|
||||||
cycle) and cycle (seconds, to a tenth). Reach never changes. Levels 3+ need refined parts.
|
cycle) and cycle (seconds, to a tenth). Reach never changes. Levels cost credits only (the refined parts they once took gated the
|
||||||
|
early game behind a refinery the player doesn't have yet; the code still takes `parts`).
|
||||||
Prices: level k of yield costs 0.97 × drill price × (Δclaim / base claim) × (base cycle /
|
Prices: level k of yield costs 0.97 × drill price × (Δclaim / base claim) × (base cycle /
|
||||||
cycle at level k−1), cycle level k costs 0.97 × price × (claim at level k−1 / base) × Δ(base
|
cycle at level k−1), cycle level k costs 0.97 × price × (claim at level k−1 / base) × Δ(base
|
||||||
cycle / cycle), × 0.95^(k−1), minus the parts' value, two significant digits, never below
|
cycle / cycle), × 0.95^(k−1), minus the parts' value, two significant digits, never below
|
||||||
|
|||||||
+1
-1
@@ -74,7 +74,7 @@ Core-loop actions and their params:
|
|||||||
|
|
||||||
| action | params | notes |
|
| action | params | notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `buy-moon` | `grade` (optional: `D`, `C`, `B`, `A`, `S`; default `state.moonGrade`, the grade of the newest moon, else the lowest) | view moves to the new moon; an unknown grade is a rule error |
|
| `buy-moon` | `grade` (optional: `D`, `C`, `B`, `A`, `S`; default `state.moonGrade`, the grade of the newest moon, else the lowest) | view moves to the new moon; an unknown grade is a rule error. `count` (optional): a moon pack's size (`newMoon.grades[].packs`: 5, 10, 20 once the store is open) buys that many at once, at the pack's price, each moon at least the pack's floor; the view and `reveal` show the best of them |
|
||||||
| `stake` | `tool` (tool id), `target` (well id) | deploy; any idle copy of the tool may be used. Refused ("Nothing left to mine on this well") when the drills already on the moon will take all the tool could get (its deploy card's `here` is 0, `blocked`) |
|
| `stake` | `tool` (tool id), `target` (well id) | deploy; any idle copy of the tool may be used. Refused ("Nothing left to mine on this well") when the drills already on the moon will take all the tool could get (its deploy card's `here` is 0, `blocked`) |
|
||||||
| `claim` | `claim-well` or `claim-moon` or `claim-wide` (moon id), `redeploy=1` | no scope: claim all (needs `unlocked["claim-all"]`); `redeploy` needs `unlocked.redeploy`; a tool whose well is drained or has no room left (as `stake`) stays idle |
|
| `claim` | `claim-well` or `claim-moon` or `claim-wide` (moon id), `redeploy=1` | no scope: claim all (needs `unlocked["claim-all"]`); `redeploy` needs `unlocked.redeploy`; a tool whose well is drained or has no room left (as `stake`) stays idle |
|
||||||
| `redrill` | `redrill-well` (well id) | redeploy idle tools that last mined the well, until it has no room left: each time the smallest that fills what is left alone, else the biggest (as `stake`; the rest stay idle, refused if none fits) |
|
| `redrill` | `redrill-well` (well id) | redeploy idle tools that last mined the well, until it has no room left: each time the smallest that fills what is left alone, else the biggest (as `stake`; the rest stay idle, refused if none fits) |
|
||||||
|
|||||||
+15
-1
@@ -851,13 +851,27 @@ type APIMoonGrade struct {
|
|||||||
SupplyMax float64 `json:"supplyMax"`
|
SupplyMax float64 `json:"supplyMax"`
|
||||||
Default bool `json:"default"` // what buy-moon without a grade buys
|
Default bool `json:"default"` // what buy-moon without a grade buys
|
||||||
WhyText string `json:"whyText"` // why its buy button is disabled, "" if it isn't
|
WhyText string `json:"whyText"` // why its buy button is disabled, "" if it isn't
|
||||||
|
// Moon packs (once the store is open): buy-moon with grade=<grade>&count=<count>.
|
||||||
|
Packs []APIMoonPack `json:"packs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIMoonPack is count moons of a grade bought at once, for price (discount off; each moon
|
||||||
|
// at least a little better than a single one's floor).
|
||||||
|
type APIMoonPack struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
CanAfford bool `json:"canAfford"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiMoonGrades(gs []GradeOffer) []APIMoonGrade {
|
func apiMoonGrades(gs []GradeOffer) []APIMoonGrade {
|
||||||
out := []APIMoonGrade{}
|
out := []APIMoonGrade{}
|
||||||
for _, g := range gs {
|
for _, g := range gs {
|
||||||
a := APIMoonGrade{Grade: g.Grade, Price: g.Price, CanAfford: g.CanAfford, WellsMin: g.WellsMin, WellsMax: g.WellsMax,
|
a := APIMoonGrade{Grade: g.Grade, Price: g.Price, CanAfford: g.CanAfford, WellsMin: g.WellsMin, WellsMax: g.WellsMax,
|
||||||
SupplyMin: g.SupplyMin, SupplyMax: g.SupplyMax, Default: g.Default}
|
SupplyMin: g.SupplyMin, SupplyMax: g.SupplyMax, Default: g.Default, Packs: []APIMoonPack{}}
|
||||||
|
for _, p := range g.Packs {
|
||||||
|
a.Packs = append(a.Packs, APIMoonPack{Count: p.Count, Price: p.Price, Discount: p.Discount, CanAfford: p.CanAfford})
|
||||||
|
}
|
||||||
if !g.CanAfford {
|
if !g.CanAfford {
|
||||||
a.WhyText = "Not enough credits"
|
a.WhyText = "Not enough credits"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -30,6 +31,8 @@ type GameConfig struct {
|
|||||||
StartingTools []int `json:"startingTools"` // store item ids, given free
|
StartingTools []int `json:"startingTools"` // store item ids, given free
|
||||||
// Moon vendors, cheapest first (rollWells in game.go); the first is the default grade.
|
// Moon vendors, cheapest first (rollWells in game.go); the first is the default grade.
|
||||||
MoonGrades []MoonGrade `json:"moonGrades"`
|
MoonGrades []MoonGrade `json:"moonGrades"`
|
||||||
|
// Packs of moons bought at once (buyMoons in game.go), smallest first.
|
||||||
|
MoonPacks []MoonPack `json:"moonPacks,omitempty"`
|
||||||
// Selling a moon back (resale.go).
|
// Selling a moon back (resale.go).
|
||||||
MoonResale MoonResale `json:"moonResale"`
|
MoonResale MoonResale `json:"moonResale"`
|
||||||
// Selling tools back (resale.go).
|
// Selling tools back (resale.go).
|
||||||
@@ -68,6 +71,30 @@ type MoonGrade struct {
|
|||||||
MinValue float64 `json:"minValue,omitempty"`
|
MinValue float64 `json:"minValue,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MoonPack is count moons of one grade bought at once: discount off their price, and each
|
||||||
|
// moon worth at least minValue × its grade's price (re-rolled like a grade's minValue,
|
||||||
|
// whichever floor is higher).
|
||||||
|
type MoonPack struct {
|
||||||
|
Count int `json:"count"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
MinValue float64 `json:"minValue,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// pack is the moon pack of that size (nil: none, or a single moon).
|
||||||
|
func (cfg *GameConfig) pack(count int) *MoonPack {
|
||||||
|
for i := range cfg.MoonPacks {
|
||||||
|
if cfg.MoonPacks[i].Count == count {
|
||||||
|
return &cfg.MoonPacks[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackPrice is what count moons of the grade cost in this pack.
|
||||||
|
func (p *MoonPack) PackPrice(g *MoonGrade) float64 {
|
||||||
|
return math.Round(float64(p.Count) * g.Price * (1 - p.Discount))
|
||||||
|
}
|
||||||
|
|
||||||
// grade is the moon grade with that name, else the first (lowest) one.
|
// grade is the moon grade with that name, else the first (lowest) one.
|
||||||
func (cfg *GameConfig) grade(name string) *MoonGrade {
|
func (cfg *GameConfig) grade(name string) *MoonGrade {
|
||||||
if i := cfg.gradeIndex(name); i >= 0 {
|
if i := cfg.gradeIndex(name); i >= 0 {
|
||||||
@@ -162,6 +189,14 @@ func (cfg *GameConfig) validate() error {
|
|||||||
if err := cfg.MarketPressure.validate(); err != nil {
|
if err := cfg.MarketPressure.validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
for i, p := range cfg.MoonPacks {
|
||||||
|
if p.Count < 2 || p.Discount < 0 || p.Discount >= 1 || p.MinValue < 0 || p.MinValue > 3 {
|
||||||
|
return fmt.Errorf("moon pack %d: count at least 2, discount from 0 to below 1, minValue 0 to 3", i+1)
|
||||||
|
}
|
||||||
|
if i > 0 && p.Count <= cfg.MoonPacks[i-1].Count {
|
||||||
|
return fmt.Errorf("moon packs: smallest first, no two the same size")
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := cfg.MoonResale.validate(); err != nil {
|
if err := cfg.MoonResale.validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-47
@@ -3,7 +3,7 @@
|
|||||||
"startingResources": 0,
|
"startingResources": 0,
|
||||||
"startingTools": [1],
|
"startingTools": [1],
|
||||||
"moonGrades": [
|
"moonGrades": [
|
||||||
{"grade": "D", "price": 300, "minValue": 1.3, "wellSupplyMin": 23, "wellSupplyMax": 46, "wellCountWeights": {"1": 40, "2": 60},
|
{"grade": "D", "price": 300, "minValue": 1.3, "wellSupplyMin": 28, "wellSupplyMax": 56, "wellCountWeights": {"1": 40, "2": 60},
|
||||||
"affinities": {"Sollux": 6, "Shadowstone": 5, "Azurium": 1, "Novafor": 0.4, "Nebulance": 0.15}},
|
"affinities": {"Sollux": 6, "Shadowstone": 5, "Azurium": 1, "Novafor": 0.4, "Nebulance": 0.15}},
|
||||||
{"grade": "C", "price": 12000, "minValue": 1.3, "wellSupplyMin": 432, "wellSupplyMax": 860, "wellCountWeights": {"2": 50, "3": 50},
|
{"grade": "C", "price": 12000, "minValue": 1.3, "wellSupplyMin": 432, "wellSupplyMax": 860, "wellCountWeights": {"2": 50, "3": 50},
|
||||||
"affinities": {"Sollux": 3, "Shadowstone": 3, "Azurium": 2, "Novafor": 1, "Nebulance": 0.4}},
|
"affinities": {"Sollux": 3, "Shadowstone": 3, "Azurium": 2, "Novafor": 1, "Nebulance": 0.4}},
|
||||||
@@ -14,6 +14,8 @@
|
|||||||
{"grade": "S", "price": 24000000, "wellSupplyMin": 337840, "wellSupplyMax": 829840, "wellCountWeights": {"4": 50, "5": 50},
|
{"grade": "S", "price": 24000000, "wellSupplyMin": 337840, "wellSupplyMax": 829840, "wellCountWeights": {"4": 50, "5": 50},
|
||||||
"affinities": {"Sollux": 0.4, "Shadowstone": 0.6, "Azurium": 1, "Novafor": 2, "Nebulance": 3.5}}
|
"affinities": {"Sollux": 0.4, "Shadowstone": 0.6, "Azurium": 1, "Novafor": 2, "Nebulance": 3.5}}
|
||||||
],
|
],
|
||||||
|
"moonPacks": [{"count": 5, "discount": 0.05, "minValue": 1.4}, {"count": 10, "discount": 0.1, "minValue": 1.5},
|
||||||
|
{"count": 20, "discount": 0.15, "minValue": 1.6}],
|
||||||
"moonResale": {"factor": 0.08, "maxRatio": 0.9, "minHoldSecs": 600},
|
"moonResale": {"factor": 0.08, "maxRatio": 0.9, "minHoldSecs": 600},
|
||||||
"equipmentResale": {"factor": 0.5},
|
"equipmentResale": {"factor": 0.5},
|
||||||
"marketVolatility": 0.25,
|
"marketVolatility": 0.25,
|
||||||
@@ -33,20 +35,20 @@
|
|||||||
"image": "shovel.png",
|
"image": "shovel.png",
|
||||||
"price": 1200,
|
"price": 1200,
|
||||||
"claimAmount": 8,
|
"claimAmount": 8,
|
||||||
"completionTimeInSecs": 3,
|
"completionTimeInSecs": 3, "reach": 5,
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"yield": {"name": "Twin rotors", "levels": [
|
"yield": {"name": "Twin rotors", "levels": [
|
||||||
{"value": 10, "price": 280},
|
{"value": 10, "price": 280},
|
||||||
{"value": 12, "price": 280},
|
{"value": 12, "price": 280},
|
||||||
{"value": 14, "price": 290, "parts": {"Dusk Alloy": 1}},
|
{"value": 14, "price": 290},
|
||||||
{"value": 16, "price": 280, "parts": {"Dusk Alloy": 2}}
|
{"value": 16, "price": 280}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Flight computer", "levels": [
|
"cycle": {"name": "Flight computer", "levels": [
|
||||||
{"value": 2.6, "price": 180},
|
{"value": 2.6, "price": 180},
|
||||||
{"value": 2.2, "price": 240},
|
{"value": 2.2, "price": 240},
|
||||||
{"value": 1.9, "price": 250, "parts": {"Dusk Alloy": 1}},
|
{"value": 1.9, "price": 250},
|
||||||
{"value": 1.7, "price": 210, "parts": {"Dusk Alloy": 1}},
|
{"value": 1.7, "price": 210},
|
||||||
{"value": 1.5, "price": 270, "parts": {"Dusk Alloy": 2}}
|
{"value": 1.5, "price": 270}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -57,21 +59,21 @@
|
|||||||
"image": "pickaxe.png",
|
"image": "pickaxe.png",
|
||||||
"price": 8000,
|
"price": 8000,
|
||||||
"claimAmount": 120,
|
"claimAmount": 120,
|
||||||
"completionTimeInSecs": 6,
|
"completionTimeInSecs": 6, "reach": 5,
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"yield": {"name": "Wider chisel", "levels": [
|
"yield": {"name": "Wider chisel", "levels": [
|
||||||
{"value": 144, "price": 1520},
|
{"value": 144, "price": 1520},
|
||||||
{"value": 168, "price": 1520},
|
{"value": 168, "price": 1520},
|
||||||
{"value": 192, "price": 1520, "parts": {"Dusk Alloy": 2}},
|
{"value": 192, "price": 1520},
|
||||||
{"value": 216, "price": 1520, "parts": {"Cryo Cell": 1}},
|
{"value": 216, "price": 1520},
|
||||||
{"value": 240, "price": 1520, "parts": {"Plasma Weave": 1}}
|
{"value": 240, "price": 1520}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Pneumatic hammer", "levels": [
|
"cycle": {"name": "Pneumatic hammer", "levels": [
|
||||||
{"value": 5.2, "price": 1160},
|
{"value": 5.2, "price": 1160},
|
||||||
{"value": 4.5, "price": 1360},
|
{"value": 4.5, "price": 1360},
|
||||||
{"value": 3.9, "price": 1560, "parts": {"Dusk Alloy": 2}},
|
{"value": 3.9, "price": 1560},
|
||||||
{"value": 3.4, "price": 1720, "parts": {"Cryo Cell": 1}},
|
{"value": 3.4, "price": 1720},
|
||||||
{"value": 3, "price": 1780, "parts": {"Plasma Weave": 1}}
|
{"value": 3, "price": 1780}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -82,21 +84,21 @@
|
|||||||
"image": "lunarlite.jpg",
|
"image": "lunarlite.jpg",
|
||||||
"price": 40200,
|
"price": 40200,
|
||||||
"claimAmount": 900,
|
"claimAmount": 900,
|
||||||
"completionTimeInSecs": 8,
|
"completionTimeInSecs": 8, "reach": 5,
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"yield": {"name": "Wide-bore bit", "levels": [
|
"yield": {"name": "Wide-bore bit", "levels": [
|
||||||
{"value": 1060, "price": 6900},
|
{"value": 1060, "price": 6900},
|
||||||
{"value": 1270, "price": 8700},
|
{"value": 1270, "price": 8700},
|
||||||
{"value": 1430, "price": 6900, "parts": {"Cryo Cell": 1}},
|
{"value": 1430, "price": 6900},
|
||||||
{"value": 1640, "price": 8700, "parts": {"Plasma Weave": 2}},
|
{"value": 1640, "price": 8700},
|
||||||
{"value": 1800, "price": 6900, "parts": {"Plasma Weave": 3}}
|
{"value": 1800, "price": 6900}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Spindle motor", "levels": [
|
"cycle": {"name": "Spindle motor", "levels": [
|
||||||
{"value": 7, "price": 5400},
|
{"value": 7, "price": 5400},
|
||||||
{"value": 6, "price": 7200},
|
{"value": 6, "price": 7200},
|
||||||
{"value": 5.2, "price": 7800, "parts": {"Cryo Cell": 1}},
|
{"value": 5.2, "price": 7800},
|
||||||
{"value": 4.6, "price": 7800, "parts": {"Plasma Weave": 2}},
|
{"value": 4.6, "price": 7800},
|
||||||
{"value": 4, "price": 9900, "parts": {"Plasma Weave": 3}}
|
{"value": 4, "price": 9900}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -108,21 +110,21 @@
|
|||||||
"price": 645000,
|
"price": 645000,
|
||||||
"claimAmount": 20000,
|
"claimAmount": 20000,
|
||||||
"completionTimeInSecs": 10,
|
"completionTimeInSecs": 10,
|
||||||
"reach": 2,
|
"reach": 5,
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"yield": {"name": "Wider beam", "levels": [
|
"yield": {"name": "Wider beam", "levels": [
|
||||||
{"value": 23700, "price": 115000},
|
{"value": 23700, "price": 115000},
|
||||||
{"value": 28200, "price": 135000},
|
{"value": 28200, "price": 135000},
|
||||||
{"value": 31800, "price": 115000, "parts": {"Cryo Cell": 2}},
|
{"value": 31800, "price": 115000},
|
||||||
{"value": 36300, "price": 135000, "parts": {"Plasma Weave": 3}},
|
{"value": 36300, "price": 135000},
|
||||||
{"value": 40000, "price": 115000, "parts": {"Void Core": 1}}
|
{"value": 40000, "price": 115000}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Laser capacitors", "levels": [
|
"cycle": {"name": "Laser capacitors", "levels": [
|
||||||
{"value": 8.7, "price": 90000},
|
{"value": 8.7, "price": 90000},
|
||||||
{"value": 7.5, "price": 115000},
|
{"value": 7.5, "price": 115000},
|
||||||
{"value": 6.5, "price": 125000, "parts": {"Cryo Cell": 2}},
|
{"value": 6.5, "price": 125000},
|
||||||
{"value": 5.7, "price": 130000, "parts": {"Plasma Weave": 3}},
|
{"value": 5.7, "price": 130000},
|
||||||
{"value": 5, "price": 150000, "parts": {"Void Core": 1}}
|
{"value": 5, "price": 150000}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -134,21 +136,21 @@
|
|||||||
"price": 9440000,
|
"price": 9440000,
|
||||||
"claimAmount": 384000,
|
"claimAmount": 384000,
|
||||||
"completionTimeInSecs": 12,
|
"completionTimeInSecs": 12,
|
||||||
"reach": 3,
|
"reach": 5,
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"yield": {"name": "Fine sieves", "levels": [
|
"yield": {"name": "Fine sieves", "levels": [
|
||||||
{"value": 452000, "price": 1600000},
|
{"value": 452000, "price": 1600000},
|
||||||
{"value": 542000, "price": 2080000},
|
{"value": 542000, "price": 2080000},
|
||||||
{"value": 610000, "price": 1600000, "parts": {"Plasma Weave": 3}},
|
{"value": 610000, "price": 1600000},
|
||||||
{"value": 700000, "price": 2080000, "parts": {"Void Core": 1}},
|
{"value": 700000, "price": 2080000},
|
||||||
{"value": 768000, "price": 1600000, "parts": {"Stellar Lattice": 1}}
|
{"value": 768000, "price": 1600000}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Auger gearbox", "levels": [
|
"cycle": {"name": "Auger gearbox", "levels": [
|
||||||
{"value": 10.4, "price": 1360000},
|
{"value": 10.4, "price": 1360000},
|
||||||
{"value": 9, "price": 1600000},
|
{"value": 9, "price": 1600000},
|
||||||
{"value": 7.8, "price": 1840000, "parts": {"Plasma Weave": 3}},
|
{"value": 7.8, "price": 1840000},
|
||||||
{"value": 6.8, "price": 2000000, "parts": {"Void Core": 1}},
|
{"value": 6.8, "price": 2000000},
|
||||||
{"value": 6, "price": 2080000, "parts": {"Stellar Lattice": 1}}
|
{"value": 6, "price": 2080000}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -165,16 +167,16 @@
|
|||||||
"yield": {"name": "Larger plates", "levels": [
|
"yield": {"name": "Larger plates", "levels": [
|
||||||
{"value": 1050000, "price": 3120000},
|
{"value": 1050000, "price": 3120000},
|
||||||
{"value": 1220000, "price": 3120000},
|
{"value": 1220000, "price": 3120000},
|
||||||
{"value": 1420000, "price": 3600000, "parts": {"Void Core": 1}},
|
{"value": 1420000, "price": 3600000},
|
||||||
{"value": 1590000, "price": 3120000, "parts": {"Stellar Lattice": 1}},
|
{"value": 1590000, "price": 3120000},
|
||||||
{"value": 1760000, "price": 3120000, "parts": {"Void Core": 2, "Stellar Lattice": 2}}
|
{"value": 1760000, "price": 3120000}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Charge coils", "levels": [
|
"cycle": {"name": "Charge coils", "levels": [
|
||||||
{"value": 12.2, "price": 2400000},
|
{"value": 12.2, "price": 2400000},
|
||||||
{"value": 10.5, "price": 2960000},
|
{"value": 10.5, "price": 2960000},
|
||||||
{"value": 9.1, "price": 3280000, "parts": {"Void Core": 1}},
|
{"value": 9.1, "price": 3280000},
|
||||||
{"value": 8, "price": 3440000, "parts": {"Stellar Lattice": 1}},
|
{"value": 8, "price": 3440000},
|
||||||
{"value": 7, "price": 4000000, "parts": {"Void Core": 2, "Stellar Lattice": 2}}
|
{"value": 7, "price": 4000000}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -191,16 +193,16 @@
|
|||||||
"yield": {"name": "Core barrel", "levels": [
|
"yield": {"name": "Core barrel", "levels": [
|
||||||
{"value": 1910000, "price": 4480000},
|
{"value": 1910000, "price": 4480000},
|
||||||
{"value": 2250000, "price": 5040000},
|
{"value": 2250000, "price": 5040000},
|
||||||
{"value": 2550000, "price": 4480000, "parts": {"Void Core": 2}},
|
{"value": 2550000, "price": 4480000},
|
||||||
{"value": 2890000, "price": 5040000, "parts": {"Stellar Lattice": 2}},
|
{"value": 2890000, "price": 5040000},
|
||||||
{"value": 3200000, "price": 4480000, "parts": {"Void Core": 3, "Stellar Lattice": 3}}
|
{"value": 3200000, "price": 4480000}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Drill string", "levels": [
|
"cycle": {"name": "Drill string", "levels": [
|
||||||
{"value": 14, "price": 3360000},
|
{"value": 14, "price": 3360000},
|
||||||
{"value": 12, "price": 4480000},
|
{"value": 12, "price": 4480000},
|
||||||
{"value": 10.4, "price": 4800000, "parts": {"Void Core": 2}},
|
{"value": 10.4, "price": 4800000},
|
||||||
{"value": 9.1, "price": 5200000, "parts": {"Stellar Lattice": 2}},
|
{"value": 9.1, "price": 5200000},
|
||||||
{"value": 8, "price": 5680000, "parts": {"Void Core": 3, "Stellar Lattice": 3}}
|
{"value": 8, "price": 5680000}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+93
-48
@@ -616,11 +616,17 @@ func createUser(ctx context.Context, db *pgxpool.Pool, name string) (string, err
|
|||||||
// grade with a minValue rolls again while the moon is worth less than that × its price at
|
// grade with a minValue rolls again while the moon is worth less than that × its price at
|
||||||
// base prices (at most 64 times; then the best roll).
|
// base prices (at most 64 times; then the best roll).
|
||||||
func rollWells(c *GameConfig, g *MoonGrade, kind MoonType, first bool, rnd func() float64) []wellSupply {
|
func rollWells(c *GameConfig, g *MoonGrade, kind MoonType, first bool, rnd func() float64) []wellSupply {
|
||||||
|
return rollWellsAtLeast(c, g, kind, first, rnd, g.MinValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rollWellsAtLeast is rollWells with the floor minValue × the grade's price (a moon pack's
|
||||||
|
// is higher than its grade's).
|
||||||
|
func rollWellsAtLeast(c *GameConfig, g *MoonGrade, kind MoonType, first bool, rnd func() float64, minValue float64) []wellSupply {
|
||||||
wells := rollWellsOnce(c, g, kind, first, rnd)
|
wells := rollWellsOnce(c, g, kind, first, rnd)
|
||||||
if g.MinValue <= 0 {
|
if minValue <= 0 {
|
||||||
return wells
|
return wells
|
||||||
}
|
}
|
||||||
floor, best := g.MinValue*g.Price, wellsValue(c, wells)
|
floor, best := minValue*g.Price, wellsValue(c, wells)
|
||||||
for try := 0; best < floor && try < 64; try++ {
|
for try := 0; best < floor && try < 64; try++ {
|
||||||
w := rollWellsOnce(c, g, kind, first, rnd)
|
w := rollWellsOnce(c, g, kind, first, rnd)
|
||||||
if v := wellsValue(c, w); v > best {
|
if v := wellsValue(c, w); v > best {
|
||||||
@@ -739,13 +745,32 @@ func rollMoonName(n MoonNames, rnd func() float64) string {
|
|||||||
// grade is the moon grade's name ("D", …); "" buys the grade of the player's newest moon,
|
// grade is the moon grade's name ("D", …); "" buys the grade of the player's newest moon,
|
||||||
// or the lowest.
|
// or the lowest.
|
||||||
func buyMoon(ctx context.Context, db *pgxpool.Pool, userID, grade string) (string, error) {
|
func buyMoon(ctx context.Context, db *pgxpool.Pool, userID, grade string) (string, error) {
|
||||||
var moonID string
|
ids, err := buyMoons(ctx, db, userID, grade, 1)
|
||||||
if grade != "" && cfg().gradeIndex(grade) < 0 {
|
if err != nil {
|
||||||
return "", gameErr("Unknown moon grade")
|
return "", err
|
||||||
}
|
}
|
||||||
|
return ids[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buyMoons claims count moons of the grade at once: 1, or a moon pack's size (config
|
||||||
|
// moonPacks: its discount off the price, its floor on each moon's value). It returns the
|
||||||
|
// moons' ids, the best rated first. Packs come with the store.
|
||||||
|
func buyMoons(ctx context.Context, db *pgxpool.Pool, userID, grade string, count int) ([]string, error) {
|
||||||
|
if grade != "" && cfg().gradeIndex(grade) < 0 {
|
||||||
|
return nil, gameErr("Unknown moon grade")
|
||||||
|
}
|
||||||
|
var pack *MoonPack
|
||||||
|
if count != 1 {
|
||||||
|
if pack = cfg().pack(count); pack == nil {
|
||||||
|
return nil, gameErr("No moon pack of that size")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var ids []string
|
||||||
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
||||||
|
ids = nil
|
||||||
// Locking the player's row keeps moon names unique and the moon count right.
|
// Locking the player's row keeps moon names unique and the moon count right.
|
||||||
_, err := tx.Exec(ctx, "select 1 from users where id = $1 for no key update", userID)
|
var onboarding []string
|
||||||
|
err := tx.QueryRow(ctx, "select onboarding from users where id = $1 for no key update", userID).Scan(&onboarding)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -754,6 +779,9 @@ func buyMoon(ctx context.Context, db *pgxpool.Pool, userID, grade string) (strin
|
|||||||
if err := newMoonAllowed(ctx, tx, userID); err != nil {
|
if err := newMoonAllowed(ctx, tx, userID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if pack != nil && !(&State{Onboarding: onboarding}).Unlocked("store") {
|
||||||
|
return gameErr("Not available yet")
|
||||||
|
}
|
||||||
var owned int
|
var owned int
|
||||||
var last string
|
var last string
|
||||||
err = tx.QueryRow(ctx, `
|
err = tx.QueryRow(ctx, `
|
||||||
@@ -767,13 +795,15 @@ func buyMoon(ctx context.Context, db *pgxpool.Pool, userID, grade string) (strin
|
|||||||
grade = last
|
grade = last
|
||||||
}
|
}
|
||||||
g := cfg().grade(grade)
|
g := cfg().grade(grade)
|
||||||
price := g.Price
|
price, floor := g.Price, g.MinValue
|
||||||
|
if pack != nil {
|
||||||
|
price, floor = pack.PackPrice(g), max(floor, pack.MinValue)
|
||||||
|
}
|
||||||
if err := debit(ctx, tx, userID, price); err != nil {
|
if err := debit(ctx, tx, userID, price); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
kind := cfg().Moons[mrand.IntN(len(cfg().Moons))]
|
// Names the player hasn't used yet (the row lock above keeps them so).
|
||||||
// A name the player hasn't used yet (the row lock above keeps it so).
|
|
||||||
rows, err := tx.Query(ctx, "select name from staking_source where user_id = $1", userID)
|
rows, err := tx.Query(ctx, "select name from staking_source where user_id = $1", userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -782,52 +812,67 @@ func buyMoon(ctx context.Context, db *pgxpool.Pool, userID, grade string) (strin
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
name := rollMoonName(cfg().MoonNames, mrand.Float64)
|
best, bestRating := -1, 0
|
||||||
for i := 2; slices.Contains(taken, name); i++ {
|
for n := range count {
|
||||||
if i <= 20 {
|
kind := cfg().Moons[mrand.IntN(len(cfg().Moons))]
|
||||||
name = rollMoonName(cfg().MoonNames, mrand.Float64)
|
name := rollMoonName(cfg().MoonNames, mrand.Float64)
|
||||||
} else {
|
for i := 2; slices.Contains(taken, name); i++ {
|
||||||
name = fmt.Sprintf("%s %d", cfg().MoonNames.Roots[0], i)
|
if i <= 20 {
|
||||||
|
name = rollMoonName(cfg().MoonNames, mrand.Float64)
|
||||||
|
} else {
|
||||||
|
name = fmt.Sprintf("%s %d", cfg().MoonNames.Roots[0], i)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
taken = append(taken, name)
|
||||||
err = tx.QueryRow(ctx, `
|
var moonID string
|
||||||
insert into staking_source(user_id, name, kind, grade, description, address)
|
err = tx.QueryRow(ctx, `
|
||||||
values ($1, $2, $3, $4, $5, $6) returning id`,
|
insert into staking_source(user_id, name, kind, grade, description, address)
|
||||||
userID, name, kind.Name, g.Grade, kind.Description, "0x"+randomHex(8)).Scan(&moonID)
|
values ($1, $2, $3, $4, $5, $6) returning id`,
|
||||||
if err != nil {
|
userID, name, kind.Name, g.Grade, kind.Description, "0x"+randomHex(8)).Scan(&moonID)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
wells := rollWells(cfg(), g, kind, owned == 0, mrand.Float64)
|
|
||||||
rating := moonRating(g, wells)
|
|
||||||
for _, w := range wells {
|
|
||||||
tag, err := tx.Exec(ctx, `
|
|
||||||
insert into resource_well(source_id, resource_id, supply)
|
|
||||||
select $1, id, $3 from resource where name = $2`,
|
|
||||||
moonID, w.Resource, w.Supply)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if tag.RowsAffected() != 1 {
|
|
||||||
return fmt.Errorf("no resource %q for a well", w.Resource)
|
wells := rollWellsAtLeast(cfg(), g, kind, owned+n == 0, mrand.Float64, floor)
|
||||||
|
rating := moonRating(g, wells)
|
||||||
|
for _, w := range wells {
|
||||||
|
tag, err := tx.Exec(ctx, `
|
||||||
|
insert into resource_well(source_id, resource_id, supply)
|
||||||
|
select $1, id, $3 from resource where name = $2`,
|
||||||
|
moonID, w.Resource, w.Supply)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() != 1 {
|
||||||
|
return fmt.Errorf("no resource %q for a well", w.Resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Describe the moon from its look and wells (moondesc.go).
|
||||||
|
desc, err := moonDescription(ctx, tx, moonID, g.MeanSupply())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(ctx, "update staking_source set description = $1, rating = $3 where id = $2",
|
||||||
|
desc, moonID, rating)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ev := map[string]any{"moon": moonID, "price": price / float64(count), "rating": rating, "grade": g.Grade}
|
||||||
|
if pack != nil {
|
||||||
|
ev["pack"] = count
|
||||||
|
}
|
||||||
|
if err := logEvent(ctx, tx, userID, "buy_moon", ev); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ids = append(ids, moonID)
|
||||||
|
if best < 0 || rating > bestRating {
|
||||||
|
best, bestRating = len(ids)-1, rating
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Describe the moon from its look and wells (moondesc.go).
|
ids[0], ids[best] = ids[best], ids[0]
|
||||||
desc, err := moonDescription(ctx, tx, moonID, g.MeanSupply())
|
return unlockRefineryAt(ctx, tx, userID, g.Grade)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(ctx, "update staking_source set description = $1, rating = $3 where id = $2",
|
|
||||||
desc, moonID, rating)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := unlockRefineryAt(ctx, tx, userID, g.Grade); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return logEvent(ctx, tx, userID, "buy_moon", map[string]any{"moon": moonID, "price": price, "rating": rating, "grade": g.Grade})
|
|
||||||
})
|
})
|
||||||
return moonID, err
|
return ids, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func buyTool(ctx context.Context, db *pgxpool.Pool, userID string, storeItemID int) (string, error) {
|
func buyTool(ctx context.Context, db *pgxpool.Pool, userID string, storeItemID int) (string, error) {
|
||||||
|
|||||||
+18
-2
@@ -36,6 +36,15 @@ type GradeOffer struct {
|
|||||||
SupplyMin float64 // a well holds from SupplyMin to just under SupplyMax
|
SupplyMin float64 // a well holds from SupplyMin to just under SupplyMax
|
||||||
SupplyMax float64
|
SupplyMax float64
|
||||||
Default bool // what a buy without a grade gets
|
Default bool // what a buy without a grade gets
|
||||||
|
Packs []PackOffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackOffer is a moon pack of a grade (config moonPacks), once the store is open.
|
||||||
|
type PackOffer struct {
|
||||||
|
Count int
|
||||||
|
Price float64
|
||||||
|
Discount float64
|
||||||
|
CanAfford bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GradeOffers lists every moon grade, cheapest first.
|
// GradeOffers lists every moon grade, cheapest first.
|
||||||
@@ -44,9 +53,16 @@ func (s *State) GradeOffers() []GradeOffer {
|
|||||||
var out []GradeOffer
|
var out []GradeOffer
|
||||||
for _, g := range cfg().MoonGrades {
|
for _, g := range cfg().MoonGrades {
|
||||||
lo, hi := g.wellCountRange()
|
lo, hi := g.wellCountRange()
|
||||||
out = append(out, GradeOffer{Grade: g.Grade, Price: g.Price, CanAfford: s.Balance >= g.Price,
|
o := GradeOffer{Grade: g.Grade, Price: g.Price, CanAfford: s.Balance >= g.Price,
|
||||||
WellsMin: lo, WellsMax: hi, SupplyMin: g.WellSupplyMin, SupplyMax: g.WellSupplyMax,
|
WellsMin: lo, WellsMax: hi, SupplyMin: g.WellSupplyMin, SupplyMax: g.WellSupplyMax,
|
||||||
Default: g.Grade == def})
|
Default: g.Grade == def}
|
||||||
|
if s.Unlocked("store") {
|
||||||
|
for _, p := range cfg().MoonPacks {
|
||||||
|
price := p.PackPrice(&g)
|
||||||
|
o.Packs = append(o.Packs, PackOffer{Count: p.Count, Price: price, Discount: p.Discount, CanAfford: s.Balance >= price})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, o)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-3
@@ -346,9 +346,18 @@ func (s *server) runAction(ctx context.Context, userID, action string, get func(
|
|||||||
switch action {
|
switch action {
|
||||||
case "refused":
|
case "refused":
|
||||||
case "buy-moon":
|
case "buy-moon":
|
||||||
var moonID string
|
count := 1
|
||||||
if moonID, err = buyMoon(ctx, s.db, userID, get("grade")); err == nil {
|
if c := get("count"); c != "" {
|
||||||
message, view = "New moon claimed!", View{Section: "moons", Moon: moonID}
|
if count, err = strconv.Atoi(c); err != nil {
|
||||||
|
count = 0 // (refused: no pack of that size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var ids []string
|
||||||
|
if ids, err = buyMoons(ctx, s.db, userID, get("grade"), count); err == nil {
|
||||||
|
message, view = "New moon claimed!", View{Section: "moons", Moon: ids[0]}
|
||||||
|
if len(ids) > 1 {
|
||||||
|
message = fmt.Sprintf("%d new moons claimed!", len(ids))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "sell-moon":
|
case "sell-moon":
|
||||||
var name string
|
var name string
|
||||||
|
|||||||
Reference in new issue
Block a user