Compare commits
16
Commits
04dee47571
...
9b6b0b1204
No files matched your search
@@ -5,7 +5,7 @@
|
|||||||
-- on top instead (the web's card). card.grid lays cards out in columns (as many as
|
-- on top instead (the web's card). card.grid lays cards out in columns (as many as
|
||||||
-- card_col_min allows, like the web's grid; centered and capped by the caller), equal
|
-- card_col_min allows, like the web's grid; centered and capped by the caller), equal
|
||||||
-- heights per row, and
|
-- heights per row, and
|
||||||
-- registers them for the panel entrance (g.frames, staggered). Store, Equipment and
|
-- registers them for the panel entrance (g.frames, staggered). Store and
|
||||||
-- Advisors use it; the Refinery's recipes too (opts media = 0: no image).
|
-- Advisors use it; the Refinery's recipes too (opts media = 0: no image).
|
||||||
--
|
--
|
||||||
-- y = card.grid(g, parent, x, y, inner, items, function(c, item)
|
-- y = card.grid(g, parent, x, y, inner, items, function(c, item)
|
||||||
|
|||||||
+13
-23
@@ -202,31 +202,21 @@ end
|
|||||||
-- earlier ones left. Wells are indexes into moon.wells (the server's order).
|
-- earlier ones left. Wells are indexes into moon.wells (the server's order).
|
||||||
local EPS = 1e-9
|
local EPS = 1e-9
|
||||||
|
|
||||||
-- The wells a stake with this reach mines: its anchor first, then the richest (ties by
|
-- The wells a stake with this reach mines: its anchor first, then the next wells in the
|
||||||
-- order), leaving out empty ones.
|
-- moon's order (wrapping round), leaving out empty ones (game.go coverWells).
|
||||||
local function cover_wells(anchor, supply, reach)
|
local function cover_wells(anchor, supply, reach)
|
||||||
local others = {}
|
local n = #supply
|
||||||
for i, x in ipairs(supply) do
|
local start = (anchor and anchor >= 1 and anchor <= n) and anchor or 1
|
||||||
if i ~= anchor and x > 0 then
|
|
||||||
others[#others + 1] = i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.sort(others, function(a, b)
|
|
||||||
if supply[a] ~= supply[b] then
|
|
||||||
return supply[a] > supply[b]
|
|
||||||
end
|
|
||||||
return a < b
|
|
||||||
end)
|
|
||||||
local out = {}
|
local out = {}
|
||||||
if anchor and supply[anchor] and supply[anchor] > 0 then
|
local want = math.max(1, reach or 1)
|
||||||
out[1] = anchor
|
for k = 0, n - 1 do
|
||||||
end
|
if #out >= want then
|
||||||
for _, i in ipairs(others) do
|
break
|
||||||
out[#out + 1] = i
|
end
|
||||||
end
|
local i = (start - 1 + k) % n + 1
|
||||||
local n = math.min(#out, math.max(1, reach or 1))
|
if supply[i] > 0 then
|
||||||
for i = #out, n + 1, -1 do
|
out[#out + 1] = i
|
||||||
out[i] = nil
|
end
|
||||||
end
|
end
|
||||||
return out
|
return out
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
-- (M.register; components/ and core/hooks do it): buttons "act:<action>?<params>",
|
-- (M.register; components/ and core/hooks do it): buttons "act:<action>?<params>",
|
||||||
-- links "nav:<section>[/<what>]", tabs "tab:<section>", moon rows "moon:<id>", wells
|
-- links "nav:<section>[/<what>]", tabs "tab:<section>", moon rows "moon:<id>", wells
|
||||||
-- "well:<id>", fields "field:<key>", dialog buttons "dialog:ok" / "dialog:cancel" /
|
-- "well:<id>", fields "field:<key>", dialog buttons "dialog:ok" / "dialog:cancel" /
|
||||||
-- "dialog:<...>", "list:active|past", equipment cards "drill:<tool id>", buttons that ask
|
-- "dialog:<...>", "list:active|past", equipment rows "drill:<tool id>", buttons that ask
|
||||||
-- first "confirm:<action>", the gear "settings" (its menu: "mute", "theme", "motion",
|
-- first "confirm:<action>" (a row's Sell "confirm:sell-tool:<tool id>"), the gear "settings" (its menu: "mute", "theme", "motion",
|
||||||
-- "grant", "menu:help", "menu:save-code", "menu:restore"), "idle-copy", "better-well". A
|
-- "grant", "menu:help", "menu:save-code", "menu:restore"), "idle-copy", "better-well". A
|
||||||
-- component given opts.id uses that instead.
|
-- component given opts.id uses that instead.
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ M.size = {
|
|||||||
market_max = 1320, -- the market's (quotes, detail and ticket)
|
market_max = 1320, -- the market's (quotes, detail and ticket)
|
||||||
market_panel_max = 460, -- tallest the market's detail and ticket panels get
|
market_panel_max = 460, -- tallest the market's detail and ticket panels get
|
||||||
text_max = 720, -- widest a paragraph gets
|
text_max = 720, -- widest a paragraph gets
|
||||||
cards_max = 1400, -- widest a card grid gets (Store, Equipment, Advisors), centered
|
cards_max = 1400, -- widest a card grid gets (Store, Advisors), centered
|
||||||
card_col_min = 250, -- narrowest card column (the web's minmax(250px, 1fr)); more as it widens
|
card_col_min = 250, -- narrowest card column (the web's minmax(250px, 1fr)); more as it widens
|
||||||
card_min_w = 420, -- narrowest card with the image beside the text (narrower: image on top)
|
card_min_w = 420, -- narrowest card with the image beside the text (narrower: image on top)
|
||||||
panel_pad = 14,
|
panel_pad = 14,
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ M.button_looks = setmetatable({}, { __mode = "k" })
|
|||||||
-- A button's nodes (components/button.lua gives it behavior: hover, press, busy, clicks;
|
-- A button's nodes (components/button.lua gives it behavior: hover, press, busy, clicks;
|
||||||
-- or H.action / H.link). opts: variant ("primary", "secondary", "redrill", "danger",
|
-- or H.action / H.link). opts: variant ("primary", "secondary", "redrill", "danger",
|
||||||
-- "ghost"; theme.button), primary (= variant "primary"), small, disabled, w, h, price,
|
-- "ghost"; theme.button), primary (= variant "primary"), small, disabled, w, h, price,
|
||||||
-- accent, on (a lit ghost), fill / edge / text (override the variant's colors). Returns
|
-- accent, bare (the price right after the label, no "·"), on (a lit ghost), fill / edge / text (override the variant's colors). Returns
|
||||||
-- root, nodes, width.
|
-- root, nodes, width.
|
||||||
function M.button(parent, label, x, y, opts)
|
function M.button(parent, label, x, y, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
@@ -199,12 +199,16 @@ function M.button(parent, label, x, y, opts)
|
|||||||
local parts = { { text, M.text_width(text) } }
|
local parts = { { text, M.text_width(text) } }
|
||||||
if opts.price then
|
if opts.price then
|
||||||
local accent = opts.accent or v.price
|
local accent = opts.accent or v.price
|
||||||
local sep = M.text(root, "·", { font = "sans_semi", px = px, color = "text" }, 0, 0)
|
|
||||||
gui.set_color(sep, M.color_spec(tc))
|
|
||||||
local sym = M.text(root, theme.currency, { font = "sym", px = px - 1, color = accent }, 0, 0)
|
local sym = M.text(root, theme.currency, { font = "sym", px = px - 1, color = accent }, 0, 0)
|
||||||
local num = M.text(root, M.money(opts.price), { font = "mono", px = px - 1, color = accent }, 0, 0)
|
local num = M.text(root, M.money(opts.price), { font = "mono", px = px - 1, color = accent }, 0, 0)
|
||||||
parts[#parts + 1] = { sep, M.text_width(sep), 8 }
|
if opts.bare then -- (no "·" between: "+ ₥290")
|
||||||
parts[#parts + 1] = { sym, M.text_width(sym), 8 }
|
parts[#parts + 1] = { sym, M.text_width(sym), 5 }
|
||||||
|
else
|
||||||
|
local sep = M.text(root, "·", { font = "sans_semi", px = px, color = "text" }, 0, 0)
|
||||||
|
gui.set_color(sep, M.color_spec(tc))
|
||||||
|
parts[#parts + 1] = { sep, M.text_width(sep), 8 }
|
||||||
|
parts[#parts + 1] = { sym, M.text_width(sym), 8 }
|
||||||
|
end
|
||||||
parts[#parts + 1] = { num, M.text_width(num), 1 }
|
parts[#parts + 1] = { num, M.text_width(num), 1 }
|
||||||
end
|
end
|
||||||
local content = 0
|
local content = 0
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
-- well's facts (supply, what the drills leave, price, its drills and their times), the
|
-- well's facts (supply, what the drills leave, price, its drills and their times), the
|
||||||
-- moon's when no well is picked, or what the section puts there (a section module's
|
-- moon's when no well is picked, or what the section puts there (a section module's
|
||||||
-- aside / aside_below, see core/main.lua), always under the viewport. On the Moons
|
-- aside / aside_below, see core/main.lua), always under the viewport. On the Moons
|
||||||
-- section it only shows: every action is in the main panel (moons.lua's well cards).
|
-- section it only shows, but for destructive actions (Sell moon, at its foot): every
|
||||||
|
-- other action is in the main panel (moons.lua's well cards).
|
||||||
|
|
||||||
local theme = require("modules.theme")
|
local theme = require("modules.theme")
|
||||||
local ui = require("modules.ui")
|
local ui = require("modules.ui")
|
||||||
local moon_view = require("modules.moon.moon")
|
local moon_view = require("modules.moon.moon")
|
||||||
local reveal = require("screens.game.reveal")
|
local reveal = require("screens.game.reveal")
|
||||||
local moons = require("screens.game.moons")
|
local moons = require("screens.game.moons")
|
||||||
|
local moon_sale = require("screens.game.moon_sale")
|
||||||
local panel = require("components.panel")
|
local panel = require("components.panel")
|
||||||
local scroll = require("components.scroll")
|
local scroll = require("components.scroll")
|
||||||
local bar = require("components.bar")
|
local bar = require("components.bar")
|
||||||
@@ -104,6 +106,20 @@ local M11 = { font = "mono", px = 11, color = "muted" }
|
|||||||
local M12 = { font = "mono", px = 12, color = "muted" }
|
local M12 = { font = "mono", px = 12, color = "muted" }
|
||||||
local S12 = { font = "sans", px = 12, color = "muted" }
|
local S12 = { font = "sans", px = 12, color = "muted" }
|
||||||
|
|
||||||
|
-- Selling the moon (once the store is open): a line, then the sale (moon_sale.lua). Tree
|
||||||
|
-- pieces for the foot of the well or moon facts.
|
||||||
|
local function sale_kids(g, c, moon, inner, H)
|
||||||
|
if not (g.state.unlocked.store and moon.sale) then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
space(0, 14),
|
||||||
|
made({ height = 1 }, function(p, x, y, iw) ui.rect(p, x, y, iw, 1, "line") end),
|
||||||
|
space(0, 12),
|
||||||
|
moon_sale.box(g, H, c, moon, inner),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
-- A drill on the well (34 tall; it slides in when the drill was just deployed):
|
-- A drill on the well (34 tall; it slides in when the drill was just deployed):
|
||||||
-- thumbnail, name and track levels; what it gets; time left or Ready.
|
-- thumbnail, name and track levels; what it gets; time left or Ready.
|
||||||
local function drill_line(g, c, st, d, H, enters)
|
local function drill_line(g, c, st, d, H, enters)
|
||||||
@@ -300,7 +316,10 @@ local function well_panel(g, root, pad, top, inner, H)
|
|||||||
add(space(0, 6))
|
add(space(0, 6))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- (no buttons here: every action on the well is on its card in the main panel)
|
-- (no buttons here but Sell moon: every action on the well is on its card)
|
||||||
|
for _, k in ipairs(sale_kids(g, c, moon, inner, H)) do
|
||||||
|
add(k)
|
||||||
|
end
|
||||||
local tree = col({ width = inner }, kids)
|
local tree = col({ width = inner }, kids)
|
||||||
fg.build(flex.solve(tree, pad, top), c)
|
fg.build(flex.solve(tree, pad, top), c)
|
||||||
for _, f in ipairs(enters) do
|
for _, f in ipairs(enters) do
|
||||||
@@ -369,10 +388,12 @@ local function moon_status(g, c, x, y, inner, H)
|
|||||||
fact(H, "Ready to claim", tostring(done), { color = done > 0 and "ready" or "text" }),
|
fact(H, "Ready to claim", tostring(done), { color = done > 0 and "ready" or "text" }),
|
||||||
fact(H, "Idle drills", tostring(idle)),
|
fact(H, "Idle drills", tostring(idle)),
|
||||||
}
|
}
|
||||||
if st.unlocked.store and moon.sale then
|
for _, k in ipairs(sale_kids(g, c, moon, inner, H)) do
|
||||||
kids[#kids + 1] = fact(H, "Sells for", moon.sale.total, { credits = true })
|
kids[#kids + 1] = k
|
||||||
end
|
end
|
||||||
fg.build(flex.solve(col({ width = inner }, kids), x, y), c)
|
local tree = col({ width = inner }, kids)
|
||||||
|
fg.build(flex.solve(tree, x, y), c)
|
||||||
|
return tree.y + tree.h
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A heading (h2) and a line under the viewport.
|
-- A heading (h2) and a line under the viewport.
|
||||||
@@ -437,7 +458,10 @@ function M.build(g, parent, x, y, w, h, H, mod, row)
|
|||||||
elseif moon and moon.finished then
|
elseif moon and moon.finished then
|
||||||
caption(root, pad, cy, inner, moon.name, "Mined out. Nothing left to dig.")
|
caption(root, pad, cy, inner, moon.name, "Mined out. Nothing left to dig.")
|
||||||
elseif moon then
|
elseif moon then
|
||||||
moon_status(g, root, pad, cy, inner, H)
|
-- (scrolls like the well panel: Sell moon at its foot)
|
||||||
|
local top = cy - 8
|
||||||
|
local area = scroll.area(g, root, 0, top, w, h - top - 4, "moon:" .. moon.id)
|
||||||
|
area:finish(moon_status(g, area.content, pad, 8, inner, H) + pad)
|
||||||
elseif mod and mod.aside_below and st[section] then
|
elseif mod and mod.aside_below and st[section] then
|
||||||
mod.aside_below(g, root, pad, cy, inner, limit, H)
|
mod.aside_below(g, root, pad, cy, inner, limit, H)
|
||||||
elseif section == "new-moon" then
|
elseif section == "new-moon" then
|
||||||
|
|||||||
@@ -87,14 +87,16 @@ end
|
|||||||
|
|
||||||
---------------------------------------------------------------- build
|
---------------------------------------------------------------- build
|
||||||
|
|
||||||
-- Entrances (enter.js): the main panel when the section or moon changes, the aside when
|
-- Entrances (enter.js): the main panel when the section (or on Moons the moon) changes, the aside when
|
||||||
-- the well changes (or the drill: the client's drill panel is in the aside); nothing else
|
-- the well changes (or the drill: the client's drill panel is in the aside); nothing else
|
||||||
-- in the view (pick, list, page, adv) makes an entrance. Links keep the moon like the
|
-- in the view (pick, list, page, adv) makes an entrance. Links keep the moon like the
|
||||||
-- web's (View.ToSection), else the server's first-moon default would change it. Both on
|
-- web's (View.ToSection), else the server's first-moon default would change it. Both on
|
||||||
-- the first state and after the reveal closes (g.enter_all).
|
-- the first state and after the reveal closes (g.enter_all).
|
||||||
function enter(g)
|
function enter(g)
|
||||||
local v = g.view
|
local v = g.view
|
||||||
local main_key = v.section .. "|" .. (v.moon or "")
|
-- (the moon only for the Moons section: elsewhere an action that moves the view's moon,
|
||||||
|
-- Claim all finishing one, mustn't replay the section's entrance)
|
||||||
|
local main_key = v.section .. "|" .. (v.section == "moons" and (v.moon or "") or "")
|
||||||
local well_key = (v.moon or "") .. "|" .. (v.well or "") .. "|" .. (v.drill or "") -- (the drill panel is in the aside)
|
local well_key = (v.moon or "") .. "|" .. (v.well or "") .. "|" .. (v.drill or "") -- (the drill panel is in the aside)
|
||||||
local all = g.enter_all or not g.entered
|
local all = g.enter_all or not g.entered
|
||||||
if g.reveal:hiding() then
|
if g.reveal:hiding() then
|
||||||
|
|||||||
+338
-116
@@ -1,12 +1,11 @@
|
|||||||
-- The Equipment section (state.equipment): a card per tool (copies of the same type and
|
-- The Equipment section (state.equipment): a table, a row per tool (copies too), with its
|
||||||
-- track levels share one) with its image, levels chip, status, stats (yield, cycle) and
|
-- image, name, copy number and levels, status, and its two upgrade tracks in columns (value
|
||||||
-- its two upgrade tracks side by side (tracks.go): each track's name and level, its value
|
-- now ▸ after the next level, stat_up violet; level pips), each track's Upgrade button (or
|
||||||
-- now ▸ after the next level (stat_up violet, apart from money), the Upgrade button (or
|
-- Max) and Sell under them, notes under those (the table's comment, below). A row opens the
|
||||||
-- Max level in its place) and a need line kept whether it shows, so the buttons never
|
-- drill panel (view drill=<tool id>) in the aside: stats, the two tracks, each track's
|
||||||
-- move (clicker rule). A card opens the drill panel (view drill=<tool id>) in the aside:
|
-- ladder, idle copies and their best well, and selling the drill. Before the Store is
|
||||||
-- stats, the two tracks, each track's ladder, idle copies and their best well, and
|
-- unlocked (onboarding) rows and panel leave out the Store's parts (upgrades, selling): the
|
||||||
-- selling the drill. Before the Store is unlocked (onboarding) cards and panel leave out
|
-- panel is stats, ladders and idle copies.
|
||||||
-- the Store's parts (upgrades, selling): the panel is stats, ladders and idle copies.
|
|
||||||
|
|
||||||
local theme = require("modules.theme")
|
local theme = require("modules.theme")
|
||||||
local ui = require("modules.ui")
|
local ui = require("modules.ui")
|
||||||
@@ -14,8 +13,8 @@ local tooltip = require("modules.tooltip")
|
|||||||
local cards = require("screens.game.cards")
|
local cards = require("screens.game.cards")
|
||||||
local scroll = require("components.scroll")
|
local scroll = require("components.scroll")
|
||||||
local store = require("screens.game.store")
|
local store = require("screens.game.store")
|
||||||
local chip = require("components.chip")
|
local row_c = require("components.row")
|
||||||
local card = require("components.card")
|
local blocks = require("modules.blocks")
|
||||||
|
|
||||||
local flex = require("modules.flex")
|
local flex = require("modules.flex")
|
||||||
local fg = require("modules.flex_gui")
|
local fg = require("modules.flex_gui")
|
||||||
@@ -54,14 +53,6 @@ local function stats_box(st, parent, t)
|
|||||||
return row({ wrap = true, column_gap = 16 }, kids)
|
return row({ wrap = true, column_gap = 16 }, kids)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The same at (x, y), w wide (inside a card block); returns the height used.
|
|
||||||
local function stats(st, parent, t, x, y, w)
|
|
||||||
local tree = stats_box(st, parent, t)
|
|
||||||
tree.style.width = w
|
|
||||||
fg.build(flex.solve(tree, x, y), parent)
|
|
||||||
return tree.h
|
|
||||||
end
|
|
||||||
|
|
||||||
-- A track's value: "+5" (yield per cycle) or "2.6s" (cycle).
|
-- A track's value: "+5" (yield per cycle) or "2.6s" (cycle).
|
||||||
local function track_value(tr, v)
|
local function track_value(tr, v)
|
||||||
return tr.kind == "cycle" and ui.cycle(v) or ("+" .. ui.money(v))
|
return tr.kind == "cycle" and ui.cycle(v) or ("+" .. ui.money(v))
|
||||||
@@ -148,114 +139,330 @@ local function tracks_box(self, H, st, parent, t, w)
|
|||||||
return row({ width = w, align = "start" }, kids)
|
return row({ width = w, align = "start" }, kids)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The same at (x, y) (a card's footer); returns the height used.
|
---------------------------------------------------------------- the table
|
||||||
local function tracks(self, H, st, parent, t, x, y, w)
|
|
||||||
local tree = tracks_box(self, H, st, parent, t, w)
|
-- One row per tool (copies are rows of their own), fixed columns: image | name, with
|
||||||
fg.build(flex.solve(tree, x, y), parent)
|
-- "#<copy> · Y2 C1" under it (the copy's number among tools of its type, its levels) |
|
||||||
return tree.h
|
-- Status | Yield / claim | Cycle | Sell. Line 1: each track's value ▸ next and its level
|
||||||
|
-- pips. Once the Store is open, line 2: each track's Upgrade ("+ ₥290"; Max at the top)
|
||||||
|
-- under its column and Sell in the last; line 3: a short note under each (credits short,
|
||||||
|
-- the parts it uses, why Sell is off), kept whether it shows. Every row is the same height
|
||||||
|
-- (row_h), so buttons never move (clicker rule). Narrow tables (below WIDE): Status leaves
|
||||||
|
-- its column for line 2, under the name. A click on the row outside its buttons opens the
|
||||||
|
-- drill panel. Sell asks in place (cards.ask_*): Confirm and Cancel take line 2's right
|
||||||
|
-- end (Cancel on Sell's spot), the question line 3.
|
||||||
|
|
||||||
|
local PADX, PADY = 12, 9
|
||||||
|
local LINE1, NOTE = 40, 15
|
||||||
|
local WIDE = 640 -- (narrower: no Status column)
|
||||||
|
local TABLE_MAX = 1100
|
||||||
|
|
||||||
|
local HEAD = { font = "sans_semi", px = 10, color = "muted", tracking = 0.1, upper = true }
|
||||||
|
local WHERE = { font = "sans", px = 13, color = "text_2" }
|
||||||
|
local NOTE_S = { font = "sans", px = 11, color = "muted" }
|
||||||
|
|
||||||
|
-- Column widths for the table's width (narrow ones tighter, so names have room).
|
||||||
|
local function geometry(inner)
|
||||||
|
local wide = inner >= WIDE
|
||||||
|
return {
|
||||||
|
wide = wide,
|
||||||
|
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(230, math.floor(inner * 0.22)) or 0,
|
||||||
|
track = wide and 124 or 90,
|
||||||
|
sell = wide and 150 or 60,
|
||||||
|
name_px = wide and 14 or 13,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A tool's status (a 20 tall row at (x, y - 10), w wide): where its drill is (its
|
local function row_h(open, wide)
|
||||||
-- resource dot, "<moon> · <resource>", returned as a link target) and time left or Ready,
|
if open then
|
||||||
-- or how many copies are idle.
|
return PADY * 2 + LINE1 + 6 + S.button_sm_h + 2 + NOTE
|
||||||
local function status(self, H, st, parent, t, x, y, w)
|
end
|
||||||
local d = t.stake
|
return PADY * 2 + LINE1 + (wide and 0 or 26)
|
||||||
local kids, where_link = {}, nil
|
end
|
||||||
if d then
|
|
||||||
local moon = find(st.moons, "id", d.moonId)
|
-- Column captions.
|
||||||
local where = (moon and (moon.name .. " · ") or "") .. d.resource
|
local function header(c, geo)
|
||||||
local lbl = fg.label(parent, where, { font = "sans", px = 13, color = "text_2" }, 20,
|
local function cap(text, w)
|
||||||
{ fit = true, shrink = 1, max_width = w - 14 - 110 })
|
return fg.label(c, text, HEAD, 14, { width = w, shrink = 0, fit = true })
|
||||||
lbl.done = function(n, b)
|
end
|
||||||
where_link = { node = n, x = x, y = y, w = b.w + 14, view = { section = "moons", moon = d.moonId, well = d.wellId } }
|
return row({ height = 14, gap = geo.gap, padding = { 0, PADX, 0, PADX }, align = "center" }, {
|
||||||
|
fg.label(c, geo.wide and "Drill" or "Drill · Status", HEAD, 14, { grow = 1, shrink = 1, basis = 0, fit = true }),
|
||||||
|
geo.wide and cap("Status", geo.status) or nil,
|
||||||
|
cap("Yield / claim", geo.track), cap("Cycle", geo.track), space(geo.sell),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A track's value in the table: "12" (claim per cycle) or "2.4s".
|
||||||
|
local function cell_value(tr, v)
|
||||||
|
return tr.kind == "cycle" and ui.cycle(v) or ui.money(v)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Line 1 of a track's column: value ▸ next (the next only once the Store is open), its
|
||||||
|
-- level pips under it.
|
||||||
|
local function value_cell(c, tr, w, open)
|
||||||
|
if not tr then
|
||||||
|
return space(w)
|
||||||
|
end
|
||||||
|
local n = open and tr.next
|
||||||
|
return col({ width = w, shrink = 0 }, {
|
||||||
|
made({ height = 18, shrink = 0 }, function(p, x, y)
|
||||||
|
local now = ui.text(p, cell_value(tr, tr.value), { font = "mono", px = 13, color = "text" }, x, y + 9)
|
||||||
|
if n then
|
||||||
|
local ax = x + ui.text_width(now) + 5
|
||||||
|
ax = ax + cards.arrow(p, ax, y + 9) + 4
|
||||||
|
local nx = ui.text(p, cell_value(tr, n.value), { font = "mono_semi", px = 13, color = "stat_up" }, ax, y + 9)
|
||||||
|
tooltip.add(nx, "After the upgrade")
|
||||||
|
end
|
||||||
|
end),
|
||||||
|
space(0, 3),
|
||||||
|
made({ height = 9, shrink = 0 }, function(p, x, y, bw)
|
||||||
|
local m = math.max(1, tr.max)
|
||||||
|
local pw = math.max(3, math.min(12, math.floor((bw - 3 * (m - 1)) / m)))
|
||||||
|
local hold = ui.rect(p, x, y, (pw + 3) * m - 3, 9, "none")
|
||||||
|
for i = 1, m do
|
||||||
|
ui.rect(hold, (i - 1) * (pw + 3), 2, pw, 5, i <= tr.level and "cyan" or "line_hi")
|
||||||
|
end
|
||||||
|
tooltip.add(hold, tr.name .. " " .. tr.level .. "/" .. tr.max)
|
||||||
|
end),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Line 2 of a track's column: its Upgrade button ("+ ₥290"), or Max at the top.
|
||||||
|
local function up_button(self, H, c, t, tr, w)
|
||||||
|
local n = tr and tr.next
|
||||||
|
if not n then
|
||||||
|
return box({ width = w, height = S.button_sm_h, shrink = 0, align = "center", justify = "center" }, {
|
||||||
|
tr and fg.label(c, "Max", { font = "sans_semi", px = 12, color = "muted", tracking = 0.12, upper = true }, 14) or nil,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
local ok = n.canAfford
|
||||||
|
local tip = n.label
|
||||||
|
if not ok and (n.blockedText or "") ~= "" then
|
||||||
|
tip = tip .. ": " .. n.blockedText
|
||||||
|
elseif #(n.parts or {}) > 0 then
|
||||||
|
local ps = {}
|
||||||
|
for _, pt in ipairs(n.parts) do
|
||||||
|
ps[#ps + 1] = ui.money(pt.need) .. " " .. pt.name
|
||||||
end
|
end
|
||||||
local right
|
tip = tip .. ": " .. theme.currency .. ui.money(n.price) .. " + " .. table.concat(ps, ", ")
|
||||||
if d.ready then
|
end
|
||||||
right = fg.label(parent, "Ready", { font = "sans_semi", px = 13, color = "ready" }, 20)
|
return fg.button(c, "+", { small = true, bare = true, price = n.price, disabled = not ok }, function(b, nodes)
|
||||||
else
|
tooltip.add(b, tip)
|
||||||
right = fg.label(parent, ui.dur(d["end"] - H.now(self)) .. " left", { font = "mono", px = 12, color = "muted" }, 20)
|
if ok then
|
||||||
right.done = function(n)
|
cards.pulse(nodes)
|
||||||
H.tick_text(self, n, d["end"], function(left)
|
H.action(self, b, nodes, "upgrade", { tool = t.id, track = tr.kind })
|
||||||
return ui.dur(left) .. " left"
|
end
|
||||||
end, "due")
|
end, { width = w })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A text in a w-wide slot of line 3, centered (right: at its right end), made a little
|
||||||
|
-- smaller to fit, else cut with … (one with ₥ only shrinks: rich text can't be cut).
|
||||||
|
local function note_text(text, color, w, tip, right)
|
||||||
|
return made({ width = w, height = NOTE, shrink = 0 }, function(p, x, y, bw)
|
||||||
|
local n, tw = ui.label(p, text, { font = "sans", px = 11, color = color }, 0, 0)
|
||||||
|
local rich = ui.needs_rich(text)
|
||||||
|
local k = math.min(1, bw / math.max(1, tw))
|
||||||
|
if k < 1 then
|
||||||
|
k = rich and k or math.max(k, 0.85)
|
||||||
|
local sc = gui.get_scale(n)
|
||||||
|
gui.set_scale(n, vmath.vector3(sc.x * k, sc.y * k, sc.z))
|
||||||
|
if not rich then
|
||||||
|
ui.fit_text(n, bw)
|
||||||
|
tw = ui.text_width(n) / k
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
kids = {
|
local lx = right and (bw - tw * k) or math.floor((bw - tw * k) / 2)
|
||||||
made({ width = 8, height = 8, shrink = 0 }, function(p, rx, ry) ui.rect(p, rx, ry, 8, 8, d.resource) end),
|
gui.set_position(n, vec(x + math.floor(lx), -(y + NOTE / 2)))
|
||||||
space(6), lbl, fg.fill(), right,
|
tooltip.add(n, tip)
|
||||||
t.count > 1 and fg.label(parent, " · all " .. t.count .. " drilling", { font = "sans", px = 13, color = "muted" }, 20) or nil,
|
end)
|
||||||
}
|
|
||||||
else
|
|
||||||
local busy = t.count - t.idle
|
|
||||||
kids = { fg.label(parent, busy > 0 and (t.idle .. " idle · " .. busy .. " drilling") or "Idle", { font = "sans", px = 13, color = "muted" }, 20) }
|
|
||||||
end
|
|
||||||
fg.build(flex.solve(row({ width = w, height = 20, align = "center" }, kids), x, y - 10), parent)
|
|
||||||
return where_link
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function tool_card(self, H, st, c, t, open)
|
-- Line 3 of a track's column: credits short (the server's shortText), else the parts the
|
||||||
c:image(t.image)
|
-- next level uses ("+ 2 Dusk Alloy", "+ 2 parts"; red when short), else blank.
|
||||||
-- Name, ×count, levels chip on the right (none for a base tool).
|
local function up_note(c, tr, w)
|
||||||
local tag = ui.levels_tag(t)
|
local n = tr and tr.next
|
||||||
c:block(22, function(p, bw)
|
local blank = space(w, NOTE)
|
||||||
local cnt = t.count > 1 and ("×" .. t.count) or nil
|
blank.blank = true
|
||||||
fg.build(flex.solve(row({ width = bw, height = 16, align = "center" }, {
|
if not n then
|
||||||
fg.label(p, t.name, { font = "sans_semi", px = 15, color = "text", tracking = 0.1, upper = true }, 16, { fit = true, shrink = 1 }),
|
return blank
|
||||||
cnt and space(6) or nil,
|
|
||||||
cnt and fg.label(p, cnt, { font = "mono", px = 13, color = "muted" }, 16) or nil,
|
|
||||||
fg.fill(), space(8),
|
|
||||||
tag ~= "" and fg.inline(p, 16, function(q, x, y, probing)
|
|
||||||
local lv_chip, cwid = chip.new(q, tag:sub(2), x, y, { font = "mono", px = 11, text = "text_2", pad = 5 })
|
|
||||||
if not probing then
|
|
||||||
tooltip.add(lv_chip, "Yield level " .. (t.yieldLevel or 0) .. ", cycle level " .. (t.cycleLevel or 0))
|
|
||||||
end
|
|
||||||
return cwid
|
|
||||||
end) or nil,
|
|
||||||
}), 0, 0), p)
|
|
||||||
end)
|
|
||||||
c:block(t.reach > 1 and 22 or 28, function(p, bw)
|
|
||||||
local l = status(self, H, st, p, t, 0, 10, bw)
|
|
||||||
if l then
|
|
||||||
-- "<moon> · <resource>" links to that well (the web's row-link).
|
|
||||||
c:after_bind(function()
|
|
||||||
local hit = ui.rect(p, l.x, l.y - 9, l.w, 18, "none")
|
|
||||||
local b = H.link(self, hit, l.view, "well-of:" .. t.id)
|
|
||||||
b.hover.on_mouse_hover:subscribe(function(_, on)
|
|
||||||
gui.set_color(l.node, ui.color(on and "cyan_hi" or "text_2"))
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end, 2)
|
|
||||||
local reach = cards.reach(st, t.reach)
|
|
||||||
if reach ~= "" then
|
|
||||||
c:text(reach .. " per cycle", "small", 24)
|
|
||||||
end
|
end
|
||||||
c:block(function(p, bw)
|
if n.blocked == "credits" and (n.shortText or "") ~= "" then
|
||||||
ui.rect(p, 0, 0, bw, 1, "line")
|
return note_text(n.shortText, "danger", w, n.blockedText)
|
||||||
return 6 + stats(st, p, t, 0, 4, bw) + 4
|
|
||||||
end)
|
|
||||||
c.id = "drill:" .. t.id
|
|
||||||
c.click = function()
|
|
||||||
H.navigate(self, drill_view(self, t.id))
|
|
||||||
end
|
end
|
||||||
if open then
|
local parts = n.parts or {}
|
||||||
-- The two upgrade tracks: measured on a probe; the footer sits at the row's bottom.
|
if #parts == 0 then
|
||||||
local probe = ui.rect(c.root, 0, 0, 1, 1, "none")
|
return blank
|
||||||
local fh = tracks(self, { action = function() end }, st, probe, t, 0, 0, c.bw)
|
end
|
||||||
gui.delete_node(probe)
|
local short, ps = false, {}
|
||||||
c:footer(fh + 2, function(f, bw)
|
for _, pt in ipairs(parts) do
|
||||||
tracks(self, H, st, f, t, 0, 2, bw)
|
short = short or pt.have < pt.need
|
||||||
|
ps[#ps + 1] = ui.money(pt.need) .. " " .. pt.name .. " (you have " .. ui.money(pt.have) .. ")"
|
||||||
|
end
|
||||||
|
local text = #parts == 1 and ("+ " .. ui.money(parts[1].need) .. " " .. parts[1].name) or ("+ " .. #parts .. " parts")
|
||||||
|
return note_text(text, short and "danger" or "muted", w, "Parts: " .. table.concat(ps, ", "))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Where a mining tool is: its resource dot and moon (a link to the well), h tall. The
|
||||||
|
-- name may hold Greek letters (a rich label: it isn't cut).
|
||||||
|
local function where_box(self, H, c, st, t, d, h)
|
||||||
|
local moon = find(st.moons, "id", d.moonId)
|
||||||
|
local lbl = fg.label(c, moon and moon.name or d.resource, WHERE, h, { shrink = 1, fit = true })
|
||||||
|
local name_n
|
||||||
|
lbl.done = function(n)
|
||||||
|
name_n = n
|
||||||
|
end
|
||||||
|
return made({ height = h, shrink = 1, min_width = 40, padding_left = 14, align = "center" }, function(p, x, y, w)
|
||||||
|
ui.rect(p, x, y + math.floor(h / 2) - 4, 8, 8, d.resource)
|
||||||
|
local hit = ui.rect(p, x, y, w, h, "none")
|
||||||
|
tooltip.add(hit, (moon and (moon.name .. " · ") or "") .. d.resource)
|
||||||
|
-- (the web's row-link to that well; bound after the row, so it gets its clicks)
|
||||||
|
local b = H.link(self, hit, { section = "moons", moon = d.moonId, well = d.wellId }, "well-of:" .. t.id)
|
||||||
|
b.hover.on_mouse_hover:subscribe(function(_, on)
|
||||||
|
if name_n then
|
||||||
|
pcall(gui.set_color, name_n, ui.color(on and "cyan_hi" or "text_2"))
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
|
end, { lbl })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Time left (ticking) or Ready, h tall.
|
||||||
|
local function left_box(self, H, c, d, h)
|
||||||
|
if d.ready then
|
||||||
|
return fg.label(c, "Ready", { font = "sans_semi", px = 13, color = "ready" }, h, { shrink = 0 })
|
||||||
end
|
end
|
||||||
c.selected = t.selected and self.view.drill ~= ""
|
local l = fg.label(c, ui.dur(d["end"] - H.now(self)) .. " left", { font = "mono", px = 12, color = "muted" }, h, { shrink = 0 })
|
||||||
|
l.done = function(n)
|
||||||
|
H.tick_text(self, n, d["end"], function(left)
|
||||||
|
return ui.dur(left) .. " left"
|
||||||
|
end, "due")
|
||||||
|
end
|
||||||
|
return l
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A tool's status: Idle, or where it mines and time left / Ready (wide: two lines in its
|
||||||
|
-- column; narrow: one line). style: the box's size in its row.
|
||||||
|
local function status_box(self, H, c, st, t, wide, style)
|
||||||
|
local d = t.stake
|
||||||
|
if not d then
|
||||||
|
style.height = style.height or 18
|
||||||
|
return fg.label(c, "Idle", { font = "sans", px = 13, color = "muted" }, 18, style)
|
||||||
|
end
|
||||||
|
if wide then
|
||||||
|
style.direction = "column"
|
||||||
|
return col(style, {
|
||||||
|
row({ height = 18 }, { where_box(self, H, c, st, t, d, 18) }),
|
||||||
|
row({ height = 16, padding_left = 14 }, { left_box(self, H, c, d, 16) }),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
style.height, style.align = 20, "center"
|
||||||
|
return row(style, { where_box(self, H, c, st, t, d, 20), space(8), left_box(self, H, c, d, 20) })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sell's confirm words: the server's for the panel's drill (equipment.drill.confirm), the
|
||||||
|
-- same words for any other row (tracks.go sellText; the API sends them for that one only).
|
||||||
|
local function sell_confirm(st, t)
|
||||||
|
local d = st.equipment.drill
|
||||||
|
if d and d.toolId == t.id and d.confirm then
|
||||||
|
return d.confirm
|
||||||
|
end
|
||||||
|
local price = theme.currency .. ui.money(t.resale or 0)
|
||||||
|
return { title = "Sell drill", text = "Sell this " .. t.name .. ui.levels_tag(t) .. " for " .. price .. "?", ok = "Sell for " .. price, danger = true }
|
||||||
|
end
|
||||||
|
|
||||||
|
local function tool_row(self, H, c, st, t, copy, geo, w, open, conf)
|
||||||
|
local wide, gap = geo.wide, geo.gap
|
||||||
|
local trs = t.tracks or {}
|
||||||
|
local tag = ui.levels_tag(t)
|
||||||
|
local tagline = "#" .. copy .. (tag ~= "" and (" ·" .. tag) or "")
|
||||||
|
.. ((t.reach or 1) > 1 and (" · " .. cards.wells(st, t.reach)) or "")
|
||||||
|
local name = fg.label(c, t.name, { font = "sans_semi", px = geo.name_px, color = "text", tracking = wide and 0.08 or 0.04, upper = true }, 20,
|
||||||
|
{ fit = true, shrink_text = 0.8 })
|
||||||
|
name.done = function(n)
|
||||||
|
tooltip.add(n, t.name .. ": yield level " .. (t.yieldLevel or 0) .. ", cycle level " .. (t.cycleLevel or 0))
|
||||||
|
end
|
||||||
|
local lines = {
|
||||||
|
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 }, {
|
||||||
|
name,
|
||||||
|
fg.label(c, tagline, { font = "mono", px = 12, color = "muted" }, 16, { fit = true }),
|
||||||
|
}),
|
||||||
|
wide and status_box(self, H, c, st, t, true, { width = geo.status, shrink = 0 }) or nil,
|
||||||
|
value_cell(c, trs[1], geo.track, open), value_cell(c, trs[2], geo.track, open),
|
||||||
|
space(geo.sell),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
local left = wide and fg.fill() or status_box(self, H, c, st, t, false, { grow = 1, shrink = 1, basis = 0 })
|
||||||
|
if open then
|
||||||
|
local why = (t.noSale or "") ~= "" and t.noSaleText or nil
|
||||||
|
local sid = "confirm:sell-tool:" .. t.id
|
||||||
|
local asking = not why and cards.asking(self, sid, conf, { tool = t.id })
|
||||||
|
local right_w = geo.track * 2 + geo.sell + gap * 2
|
||||||
|
lines[#lines + 1] = space(0, 6)
|
||||||
|
if asking then
|
||||||
|
local sell_w = fg.button_w(c, "Sell", { small = true, variant = "danger" })
|
||||||
|
local ok, cancel = cards.ask_buttons(self, c, conf, sell_w)
|
||||||
|
lines[#lines + 1] = row({ height = S.button_sm_h, gap = 6, align = "center" }, { left, ok, cancel })
|
||||||
|
lines[#lines + 1] = space(0, 2)
|
||||||
|
lines[#lines + 1] = row({ height = NOTE }, {
|
||||||
|
fg.fill(), box({ width = right_w, height = NOTE, shrink = 0 }, { cards.ask_text(conf, c, { lines = 1 }) }),
|
||||||
|
})
|
||||||
|
else
|
||||||
|
lines[#lines + 1] = row({ height = S.button_sm_h, gap = gap, align = "center" }, {
|
||||||
|
left,
|
||||||
|
up_button(self, H, c, t, trs[1], geo.track), up_button(self, H, c, t, trs[2], geo.track),
|
||||||
|
row({ width = geo.sell, shrink = 0, justify = "end" }, {
|
||||||
|
fg.button(c, "Sell", { small = true, disabled = why ~= nil, variant = "danger" }, function(b)
|
||||||
|
tooltip.add(b, why)
|
||||||
|
if not why then
|
||||||
|
cards.ask_button(self, b, conf, "sell-tool", { tool = t.id }, sid)
|
||||||
|
end
|
||||||
|
end),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
lines[#lines + 1] = space(0, 2)
|
||||||
|
-- (why Sell is off, right-aligned under it: over the Cycle column's note too
|
||||||
|
-- when that one is blank)
|
||||||
|
local cyc = up_note(c, trs[2], geo.track)
|
||||||
|
local wide_why = why and cyc.blank
|
||||||
|
lines[#lines + 1] = row({ height = NOTE, gap = gap }, {
|
||||||
|
fg.fill(), up_note(c, trs[1], geo.track),
|
||||||
|
not wide_why and cyc or nil,
|
||||||
|
why and note_text(why, "ready", wide_why and (geo.track + gap + geo.sell) or geo.sell, why, true)
|
||||||
|
or space(geo.sell, NOTE),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
elseif not wide then
|
||||||
|
lines[#lines + 1] = space(0, 6)
|
||||||
|
lines[#lines + 1] = row({ height = 20, align = "center" }, { left })
|
||||||
|
end
|
||||||
|
local rh = row_h(open, wide)
|
||||||
|
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)
|
||||||
|
local slot = ui.rect(p, x, y, bw, bh, "none")
|
||||||
|
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()
|
||||||
|
H.navigate(self, drill_view(self, t.id))
|
||||||
|
end })
|
||||||
|
ui.rect(body, 0, bh - 1, bw, 1, "line")
|
||||||
|
return body
|
||||||
|
end })
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.build(self, root, px, py, w, h, H)
|
function M.build(self, root, px, py, w, h, H)
|
||||||
local st = self.state
|
local st = self.state
|
||||||
local eq = st.equipment
|
local eq = st.equipment
|
||||||
local open = st.unlocked.store
|
local open = st.unlocked.store
|
||||||
local area, x, inner, y = cards.page(self, root, px, py, w, h, "equipment")
|
local area, x, inner, y = cards.page(self, root, px, py, w, h, "equipment", TABLE_MAX)
|
||||||
local c = area.content
|
local c = area.content
|
||||||
|
local geo = geometry(inner)
|
||||||
y = fg.flow(c, x, y, inner, { col({ width = inner }, { -- (full width: Buy tools sits at its right edge)
|
y = fg.flow(c, x, y, inner, { col({ width = inner }, { -- (full width: Buy tools sits at its right edge)
|
||||||
fg.label(c, "Equipment", "h1", 40),
|
fg.label(c, "Equipment", "h1", 40),
|
||||||
fg.label(c, ui.plural(#st.tools, "tool") .. "." .. (open and " Each drill has two upgrade tracks: more per cycle, or shorter cycles." or ""),
|
fg.label(c, ui.plural(#st.tools, "tool") .. "." .. (open and " Each drill has two upgrade tracks: more per cycle, or shorter cycles." or ""),
|
||||||
@@ -266,14 +473,28 @@ function M.build(self, root, px, py, w, h, H)
|
|||||||
end, { position = "absolute", top = 8, right = x + inner - (w - px) }) or nil, -- (at the panel's top-right corner, past the capped column)
|
end, { position = "absolute", top = 8, right = x + inner - (w - px) }) or nil, -- (at the panel's top-right corner, past the capped column)
|
||||||
#eq.tools == 0 and fg.label(c, "You don't own any tools yet.", "small", 16) or nil,
|
#eq.tools == 0 and fg.label(c, "You don't own any tools yet.", "small", 16) or nil,
|
||||||
#eq.tools == 0 and space(0, 14) or nil,
|
#eq.tools == 0 and space(0, 14) or nil,
|
||||||
|
#eq.tools > 0 and header(c, geo) or nil,
|
||||||
|
#eq.tools > 0 and space(0, 6) or nil,
|
||||||
}) })
|
}) })
|
||||||
y = card.grid(self, c, x, y, inner, eq.tools, function(cd, t)
|
if #eq.tools > 0 then
|
||||||
tool_card(self, H, st, cd, t, open)
|
ui.rect(c, x, y, inner, 1, "line_hi")
|
||||||
end, { key = function(t) return "eq:tool:" .. tostring(t.id) end, sig = function(t)
|
y = y + 1
|
||||||
-- (kept while what it shows stays the same: modules/blocks)
|
end
|
||||||
|
local seen, rh = {}, row_h(open, geo.wide)
|
||||||
|
for _, t in ipairs(eq.tools) do
|
||||||
|
seen[t.storeItemId] = (seen[t.storeItemId] or 0) + 1
|
||||||
|
local copy = seen[t.storeItemId]
|
||||||
|
local conf = sell_confirm(st, t)
|
||||||
local moon = t.stake and find(st.moons, "id", t.stake.moonId)
|
local moon = t.stake and find(st.moons, "id", t.stake.moonId)
|
||||||
return { open, moon and moon.name or false, st.refined, st.credits, st.resources, st.catalogVersion, self.inflight, self.view }
|
-- (kept while what it shows stays the same: modules/blocks)
|
||||||
end })
|
local sig = blocks.sig(x, y, inner, t, copy, conf, open, moon and moon.name or false, st.refined, st.credits,
|
||||||
|
st.resources, st.catalogVersion, self.inflight, self.view, self.ask and self.ask.id or false)
|
||||||
|
local ry = y
|
||||||
|
blocks.keep(self, "eq:tool:" .. tostring(t.id), sig, c, function(r)
|
||||||
|
fg.build(flex.solve(tool_row(self, H, c, st, t, copy, geo, inner, open, conf), x, ry), r)
|
||||||
|
end)
|
||||||
|
y = y + rh
|
||||||
|
end
|
||||||
area:finish(y + py)
|
area:finish(y + py)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -488,8 +709,9 @@ function M.aside(self, root, w, h, H)
|
|||||||
fg.label(c, "Sells for", { font = "sans", px = 15, color = "text" }, 16), space(6),
|
fg.label(c, "Sells for", { font = "sans", px = 15, color = "text" }, 16), space(6),
|
||||||
credits_box(c, d.resale, { px = 15 }, 16),
|
credits_box(c, d.resale, { px = 15 }, 16),
|
||||||
}),
|
}),
|
||||||
|
-- (why Sell is off, in place of the note: not only in its tooltip)
|
||||||
asking and cards.ask_text(d.confirm, c, { lines = 3 }) or fg.measured(c, function(p, x, y, bw)
|
asking and cards.ask_text(d.confirm, c, { lines = 3 }) or fg.measured(c, function(p, x, y, bw)
|
||||||
return cards.para(p, note, { font = "sans", px = 12, color = "muted" }, x, y, bw)
|
return cards.para(p, why or note, { font = "sans", px = 12, color = why and "ready" or "muted" }, x, y, bw)
|
||||||
end),
|
end),
|
||||||
space(0, 4),
|
space(0, 4),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ end
|
|||||||
|
|
||||||
local box, row, col, space, made = fg.box, fg.row, fg.col, fg.space, fg.made
|
local box, row, col, space, made = fg.box, fg.row, fg.col, fg.space, fg.made
|
||||||
|
|
||||||
-- The panel, w wide: a tree piece (moons.lua puts it under the idle drills).
|
-- The panel, w wide: a tree piece (the aside puts it at the foot of the moon's facts).
|
||||||
function M.box(self, H, c, moon, w)
|
function M.box(self, H, c, moon, w)
|
||||||
local sale = moon.sale
|
local sale = moon.sale
|
||||||
self.sale_open = self.sale_open or {}
|
self.sale_open = self.sale_open or {}
|
||||||
@@ -30,7 +30,12 @@ function M.box(self, H, c, moon, w)
|
|||||||
|
|
||||||
-- Summary: toggle (the row left of the button), offer, delta; Sell moon on the right.
|
-- Summary: toggle (the row left of the button), offer, delta; Sell moon on the right.
|
||||||
local profit = sale.delta > 0
|
local profit = sale.delta > 0
|
||||||
local dtxt = (profit and "+" or "−") .. theme.currency .. ui.money(math.abs(sale.delta)) .. (profit and " profit" or " vs a new moon")
|
-- (narrow, in the aside: no "· offer", and the delta without its words; its tooltip has them)
|
||||||
|
local narrow = w < 460
|
||||||
|
local dtxt = (profit and "+" or "−") .. theme.currency .. ui.money(math.abs(sale.delta))
|
||||||
|
.. (narrow and "" or (profit and " profit" or " vs a new moon"))
|
||||||
|
-- Sell moon off: why, on a line under the summary (not only in the button's tooltip).
|
||||||
|
local why = (not sale.canSell) and (sale.whyText or "") ~= "" and sale.whyText or nil
|
||||||
-- Sell moon asks in place (cards.ask_button): the question where the offer was, Confirm
|
-- Sell moon asks in place (cards.ask_button): the question where the offer was, Confirm
|
||||||
-- left of the button's spot, Cancel on it (the row's toggle is off meanwhile).
|
-- left of the button's spot, Cancel on it (the row's toggle is off meanwhile).
|
||||||
local asking = sale.canSell and cards.asking(self, "sell-moon", sale.confirm, { ["sell-moon"] = moon.id })
|
local asking = sale.canSell and cards.asking(self, "sell-moon", sale.confirm, { ["sell-moon"] = moon.id })
|
||||||
@@ -60,11 +65,24 @@ function M.box(self, H, c, moon, w)
|
|||||||
gui.set_euler(chev, vmath.vector3(0, 0, -90))
|
gui.set_euler(chev, vmath.vector3(0, 0, -90))
|
||||||
end
|
end
|
||||||
end),
|
end),
|
||||||
row({ height = 48, padding = { 0, 140, 0, 30 }, align = "center" }, {
|
row({ height = 48, padding = { 0, 156, 0, 30 }, align = "center" }, {
|
||||||
fg.label(c, "Sell moon", { font = "sans_semi", px = 14, color = "text", tracking = 0.12, upper = true }, 16), space(8),
|
-- (the label and the offer keep their size: the delta gives way, cut to fit)
|
||||||
fg.label(c, "· offer", { font = "sans", px = 14, color = "muted" }, 16), space(8),
|
(function()
|
||||||
inline(function(p, x, y) return (ui.credits(p, sale.total, x, y, { px = 15, color = "text", font = "mono_semi" })) end), space(10),
|
local l = fg.label(c, "Sell moon", { font = "sans_semi", px = 14, color = "text", tracking = 0.12, upper = true }, 16)
|
||||||
fg.label(c, dtxt, { font = "mono", px = 12, color = profit and "up" or "danger" }, 16, { fit = true, shrink = 1 }),
|
l.style.shrink = 0
|
||||||
|
return l
|
||||||
|
end)(), space(10),
|
||||||
|
(not narrow) and fg.label(c, "· offer", { font = "sans", px = 14, color = "muted" }, 16) or nil, (not narrow) and space(8) or nil,
|
||||||
|
(function()
|
||||||
|
local b = inline(function(p, x, y) return (ui.credits(p, sale.total, x, y, { px = 15, color = "text", font = "mono_semi" })) end)
|
||||||
|
b.style.shrink = 0
|
||||||
|
return b
|
||||||
|
end)(), space(10),
|
||||||
|
(function()
|
||||||
|
local l = fg.label(c, dtxt, { font = "mono", px = 12, color = profit and "up" or "danger" }, 16, { fit = true, shrink = 1 })
|
||||||
|
l.done = function(n) tooltip.add(n, profit and "Profit over what it cost" or "Less than a new moon costs") end
|
||||||
|
return l
|
||||||
|
end)(),
|
||||||
}),
|
}),
|
||||||
fg.button(c, "Sell moon", sell_opts, function(b)
|
fg.button(c, "Sell moon", sell_opts, function(b)
|
||||||
if not sale.canSell and (sale.whyText or "") ~= "" then
|
if not sale.canSell and (sale.whyText or "") ~= "" then
|
||||||
@@ -161,7 +179,12 @@ function M.box(self, H, c, moon, w)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
return col({ width = w, padding_bottom = 4 }, { summary, details }, { make = function(p, x, y, bw, bh)
|
local why_line = (why and not asking) and col({ padding = { 0, 14, 10, 30 } }, {
|
||||||
|
fg.measured(c, function(p, x, y, bw)
|
||||||
|
return cards.para(p, why, { font = "sans", px = 12, color = "ready" }, x, y, bw)
|
||||||
|
end),
|
||||||
|
}) or nil
|
||||||
|
return col({ width = w, padding_bottom = 4 }, { summary, why_line, details }, { make = function(p, x, y, bw, bh)
|
||||||
local b = ui.rect(p, x, y, bw, bh, "cyan", 0.03)
|
local b = ui.rect(p, x, y, bw, bh, "cyan", 0.03)
|
||||||
ui.outline(b, 0, 0, bw, bh, "edge")
|
ui.outline(b, 0, 0, bw, bh, "edge")
|
||||||
self.fx:box(b) -- (the sale's sweep covers the panel)
|
self.fx:box(b) -- (the sale's sweep covers the panel)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ local fg = require("modules.flex_gui")
|
|||||||
local images = require("modules.images")
|
local images = require("modules.images")
|
||||||
local test = require("modules.test")
|
local test = require("modules.test")
|
||||||
local predict = require("modules.predict")
|
local predict = require("modules.predict")
|
||||||
local moon_sale = require("screens.game.moon_sale")
|
|
||||||
|
|
||||||
local S = theme.size
|
local S = theme.size
|
||||||
|
|
||||||
@@ -187,10 +186,10 @@ end
|
|||||||
-- the aside shows its facts.
|
-- the aside shows its facts.
|
||||||
--
|
--
|
||||||
-- Deploying: the tray under the cards lists the idle drills (grouped like copies: same
|
-- Deploying: the tray under the cards lists the idle drills (grouped like copies: same
|
||||||
-- name, track levels, yield, cycle and reach). A well's deploy slot sends the drill picked in the
|
-- name, track levels, yield, cycle and reach). Clicking a tile deploys one of them to the
|
||||||
-- tray (g.deploy_pick), or with none picked the first one-well drill in the tray that
|
-- selected well (none selected: the first well it has room on). A well's deploy slot sends
|
||||||
-- has room there (smallest first; predict.room: what it would get after the moon's drills). Picking is
|
-- the first drill in the tray that has room there, multi-well ones too (smallest first;
|
||||||
-- a toggle; the pick is forgotten once that group is used up.
|
-- predict.room: what it would get after the moon's drills).
|
||||||
|
|
||||||
local SLOT_H, SLOT_GAP = 60, 6
|
local SLOT_H, SLOT_GAP = 60, 6
|
||||||
local CARD_MIN, CARD_GAP, CARD_PAD = 210, 12, 12
|
local CARD_MIN, CARD_GAP, CARD_PAD = 210, 12, 12
|
||||||
@@ -278,11 +277,8 @@ local function deploy_choice(g, st, well, groups)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
for _, grp in ipairs(groups) do
|
for _, grp in ipairs(groups) do
|
||||||
-- (a multi-well drill goes out only when picked: it mines other wells too)
|
-- (multi-well drills too: they deploy here like any other, then mine the next wells)
|
||||||
local here, amount = 0, 0
|
local here, amount = gets_here(st, well, grp.tool)
|
||||||
if (grp.tool.reach or 1) <= 1 then
|
|
||||||
here, amount = gets_here(st, well, grp.tool)
|
|
||||||
end
|
|
||||||
if here > EPS then
|
if here > EPS then
|
||||||
return grp, here, amount, false
|
return grp, here, amount, false
|
||||||
end
|
end
|
||||||
@@ -337,7 +333,7 @@ local function deploy_slot(g, st, well, groups, H)
|
|||||||
if ok then
|
if ok then
|
||||||
local params = { tool = grp.tool.id, target = well.id }
|
local params = { tool = grp.tool.id, target = well.id }
|
||||||
H.action(g, b, bn, "stake", params)
|
H.action(g, b, bn, "stake", params)
|
||||||
tooltip.add(b, "Deploy " .. (grp.tool.name or "a drill") .. (explicit and "" or " (the smallest idle one-well drill with room here)"))
|
tooltip.add(b, "Deploy " .. (grp.tool.name or "a drill") .. (explicit and "" or " (the smallest idle drill with room here)"))
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -456,7 +452,9 @@ local function idle_slot(g, st, well, H)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
local who = #names > 0 and (table.concat(names, ", ") .. (n > #names and (" +" .. (n - #names)) or "")) or ui.plural(n, "drill")
|
local who = #names > 0 and (table.concat(names, ", ") .. (n > #names and (" +" .. (n - #names)) or "")) or ui.plural(n, "drill")
|
||||||
local can = st.unlocked.redrill and well.supply > 0
|
-- (no Redrill while the drills on the well take all it has: the server refuses it)
|
||||||
|
local full = well.supply > 0 and (well.remain or 1) <= 1e-9
|
||||||
|
local can = st.unlocked.redrill and well.supply > 0 and not full
|
||||||
return made({ height = SLOT_H, shrink = 0 }, function(p, x, y, w)
|
return made({ height = SLOT_H, shrink = 0 }, function(p, x, y, w)
|
||||||
local body = ui.rect(p, x, y, w, SLOT_H, "cyan", 0.02)
|
local body = ui.rect(p, x, y, w, SLOT_H, "cyan", 0.02)
|
||||||
ui.outline(body, 0, 0, w, SLOT_H, "line_hi")
|
ui.outline(body, 0, 0, w, SLOT_H, "line_hi")
|
||||||
@@ -469,7 +467,8 @@ local function idle_slot(g, st, well, H)
|
|||||||
H.action(g, b, bn, "redrill", { ["redrill-well"] = well.id })
|
H.action(g, b, bn, "redrill", { ["redrill-well"] = well.id })
|
||||||
tooltip.add(b, "Send the drills that last mined this well out again")
|
tooltip.add(b, "Send the drills that last mined this well out again")
|
||||||
end, { height = SLOT_BTN_H }) or row({ height = SLOT_BTN_H, align = "center" }, {
|
end, { height = SLOT_BTN_H }) or row({ height = SLOT_BTN_H, align = "center" }, {
|
||||||
fit(body, well.supply > 0 and "Last mined here" or "Last mined here · the well is empty", S11),
|
fit(body, well.supply <= 0 and "Last mined here · the well is empty"
|
||||||
|
or (full and "Last mined here · the drills on it take the rest" or "Last mined here"), S11),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
fg.build(flex.solve(tree, 0, 0), body)
|
fg.build(flex.solve(tree, 0, 0), body)
|
||||||
@@ -510,10 +509,13 @@ local function well_card(g, c, st, moon, well, max, groups, H, enters)
|
|||||||
slots[#slots + 1] = deploy_slot(g, st, well, groups, H)
|
slots[#slots + 1] = deploy_slot(g, st, well, groups, H)
|
||||||
end
|
end
|
||||||
local n, next_d = 0, next_drill(g, well.drills)
|
local n, next_d = 0, next_drill(g, well.drills)
|
||||||
|
-- Tapped out once the ready drills are claimed (nothing left, or what is left is the
|
||||||
|
-- other drills' on it, the server's `remain`): Claim only, no Redeploy.
|
||||||
|
local tapped = well.supply - ready.amount(g, well, well.cap) <= 1e-9 or (well.remain or 1) <= 1e-9
|
||||||
for _, d in ipairs(well.drills or {}) do
|
for _, d in ipairs(well.drills or {}) do
|
||||||
local due = d.ready or (d["end"] or math.huge) <= H.now(g)
|
local due = d.ready or (d["end"] or math.huge) <= H.now(g)
|
||||||
n = n + (due and 1 or 0)
|
n = n + (due and 1 or 0)
|
||||||
slots[#slots + 1] = drill_slot(g, st, d, scope, not dry and st.unlocked.redeploy, due and n or 0, d == next_d, H, enters)
|
slots[#slots + 1] = drill_slot(g, st, d, scope, not dry and not tapped and st.unlocked.redeploy, due and n or 0, d == next_d, H, enters)
|
||||||
end
|
end
|
||||||
if (well.redrill or 0) > 0 then
|
if (well.redrill or 0) > 0 then
|
||||||
slots[#slots + 1] = idle_slot(g, st, well, H)
|
slots[#slots + 1] = idle_slot(g, st, well, H)
|
||||||
@@ -556,10 +558,16 @@ local function wide_card(g, c, st, moon, inner, H, enters)
|
|||||||
local wide = moon.wide
|
local wide = moon.wide
|
||||||
local scope = { ["claim-wide"] = moon.id }
|
local scope = { ["claim-wide"] = moon.id }
|
||||||
local slots, n, next_d = {}, 0, next_drill(g, wide.drills)
|
local slots, n, next_d = {}, 0, next_drill(g, wide.drills)
|
||||||
|
-- (the moon tapped out once these are claimed: no Redeploy)
|
||||||
|
local left = 0
|
||||||
|
for _, w in ipairs(moon.wells or {}) do
|
||||||
|
left = left + w.supply
|
||||||
|
end
|
||||||
|
local tapped = left - ready.amount(g, wide, wide.cap) <= 1e-9
|
||||||
for _, d in ipairs(wide.drills or {}) do
|
for _, d in ipairs(wide.drills or {}) do
|
||||||
local due = d.ready or (d["end"] or math.huge) <= H.now(g)
|
local due = d.ready or (d["end"] or math.huge) <= H.now(g)
|
||||||
n = n + (due and 1 or 0)
|
n = n + (due and 1 or 0)
|
||||||
slots[#slots + 1] = drill_slot(g, st, d, scope, st.unlocked.redeploy, due and n or 0, d == next_d, H, enters)
|
slots[#slots + 1] = drill_slot(g, st, d, scope, not tapped and st.unlocked.redeploy, due and n or 0, d == next_d, H, enters)
|
||||||
end
|
end
|
||||||
local sw = inner - CARD_PAD * 2
|
local sw = inner - CARD_PAD * 2
|
||||||
return col({ width = inner, padding = CARD_PAD }, {
|
return col({ width = inner, padding = CARD_PAD }, {
|
||||||
@@ -581,7 +589,7 @@ end
|
|||||||
local function tray(g, c, st, groups, inner, H)
|
local function tray(g, c, st, groups, inner, H)
|
||||||
local head = row({ height = 16, align = "center" }, {
|
local head = row({ height = 16, align = "center" }, {
|
||||||
fg.label(c, "Idle drills", "h3", 12, { shrink = 0 }), space(10, 0),
|
fg.label(c, "Idle drills", "h3", 12, { shrink = 0 }), space(10, 0),
|
||||||
#groups > 0 and fit(c, g.deploy_pick and "Picked: + Deploy on a well sends it" or "Pick one, then + Deploy on a well", "small") or nil,
|
#groups > 0 and fit(c, "Click one to deploy it to the selected well", "small") or nil,
|
||||||
})
|
})
|
||||||
if #groups == 0 then
|
if #groups == 0 then
|
||||||
return col({ width = inner }, {
|
return col({ width = inner }, {
|
||||||
@@ -595,12 +603,30 @@ local function tray(g, c, st, groups, inner, H)
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
-- The well a tile deploys to: the selected one, else the first it has room on.
|
||||||
|
local moon = st.moon
|
||||||
|
local function target(t)
|
||||||
|
local wells = moon and moon.wells or {}
|
||||||
|
for _, w in ipairs(wells) do
|
||||||
|
if w.id == g.view.well then
|
||||||
|
return w
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, w in ipairs(wells) do
|
||||||
|
if w.supply > 0 and gets_here(st, w, t) > EPS then
|
||||||
|
return w
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
local tiles = {}
|
local tiles = {}
|
||||||
for _, grp in ipairs(groups) do
|
for _, grp in ipairs(groups) do
|
||||||
local t = grp.tool
|
local t = grp.tool
|
||||||
local picked = grp.key == g.deploy_pick
|
local well = target(t)
|
||||||
|
local here = well and gets_here(st, well, t) or 0
|
||||||
|
local ok = well ~= nil and well.supply > 0 and here > EPS
|
||||||
|
local picked = false
|
||||||
tiles[#tiles + 1] = made({ height = TILE_H }, function(p, x, y, w)
|
tiles[#tiles + 1] = made({ height = TILE_H }, function(p, x, y, w)
|
||||||
local b, bn = ui.button(p, "", x, y, { variant = "ghost", on = picked, w = w, h = TILE_H })
|
local b, bn = ui.button(p, "", x, y, { variant = "ghost", w = w, h = TILE_H, disabled = not ok })
|
||||||
local img = ui.rect(b, 6, 6, TILE_H - 12, TILE_H - 12, "bg")
|
local img = ui.rect(b, 6, 6, TILE_H - 12, TILE_H - 12, "bg")
|
||||||
if t.image then
|
if t.image then
|
||||||
images.set(ui.rect(img, 0, 0, TILE_H - 12, TILE_H - 12, "none"), t.image)
|
images.set(ui.rect(img, 0, 0, TILE_H - 12, TILE_H - 12, "none"), t.image)
|
||||||
@@ -617,11 +643,12 @@ local function tray(g, c, st, groups, inner, H)
|
|||||||
if picked then
|
if picked then
|
||||||
ui.outline(b, 0, 0, w, TILE_H, "cyan", 0.9)
|
ui.outline(b, 0, 0, w, TILE_H, "cyan", 0.9)
|
||||||
end
|
end
|
||||||
tooltip.add(b, (t.name or "Drill") .. (picked and " · picked (click to unpick)" or " · pick it for + Deploy"))
|
if ok then
|
||||||
button.bind(g, b, bn, { id = "pick:" .. grp.key, label = M.short_name(t), on_click = function()
|
tooltip.add(b, "Deploy " .. (t.name or "a drill") .. " to the " .. well.resource .. " well")
|
||||||
g.deploy_pick = (not picked) and grp.key or nil
|
H.action(g, b, bn, "stake", { tool = t.id, target = well.id })
|
||||||
g.dirty = true
|
else
|
||||||
end })
|
tooltip.add(b, (t.name or "Drill") .. (well and (" · no room on the " .. well.resource .. " well") or " · no room on this moon"))
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
return col({ width = inner }, { head, space(0, 8), flex.grid(inner, TILE_MIN, 8, tiles) })
|
return col({ width = inner }, { head, space(0, 8), flex.grid(inner, TILE_MIN, 8, tiles) })
|
||||||
@@ -728,14 +755,7 @@ local function moon_view(g, c, px, py, inner, min_h, H)
|
|||||||
end
|
end
|
||||||
kids[#kids + 1] = space(0, 20)
|
kids[#kids + 1] = space(0, 20)
|
||||||
kids[#kids + 1] = tray(g, c, st, groups, inner, H)
|
kids[#kids + 1] = tray(g, c, st, groups, inner, H)
|
||||||
|
-- (selling the moon, a destructive action, is in the aside: aside.lua)
|
||||||
-- Selling the moon (once the store is open), at the foot.
|
|
||||||
if st.unlocked.store and moon.sale then
|
|
||||||
kids[#kids + 1] = space(0, 22)
|
|
||||||
kids[#kids + 1] = made({ height = 1 }, function(p, x, y, iw) ui.rect(p, x, y, iw, 1, "line") end)
|
|
||||||
kids[#kids + 1] = space(0, 12)
|
|
||||||
kids[#kids + 1] = moon_sale.box(g, H, c, moon, inner)
|
|
||||||
end
|
|
||||||
|
|
||||||
local page = col({ width = inner, min_height = min_h }, kids)
|
local page = col({ width = inner, min_height = min_h }, kids)
|
||||||
fg.build(flex.solve(page, px, py), c)
|
fg.build(flex.solve(page, px, py), c)
|
||||||
@@ -749,7 +769,8 @@ end
|
|||||||
-- Section contract (AGENTS.md, "Game screen"): root is the main panel, (px, py) its
|
-- Section contract (AGENTS.md, "Game screen"): root is the main panel, (px, py) its
|
||||||
-- padding, w x h its size.
|
-- padding, w x h its size.
|
||||||
function M.build(g, root, px, py, w, h, H)
|
function M.build(g, root, px, py, w, h, H)
|
||||||
local inner = math.min(w - px * 2, S.content_max)
|
-- (as wide as a card grid gets: a moon's well cards, 5 across where there is room)
|
||||||
|
local inner = math.min(w - px * 2, S.cards_max)
|
||||||
px = px + math.floor((w - px * 2 - inner) / 2) -- capped content is centered
|
px = px + math.floor((w - px * 2 - inner) / 2) -- capped content is centered
|
||||||
local st = g.state
|
local st = g.state
|
||||||
M.track_drills(g)
|
M.track_drills(g)
|
||||||
|
|||||||
@@ -488,9 +488,9 @@ end
|
|||||||
-- "Owned N" on the recipe cards: fx counters ("refined:<item>", bound after the build), so
|
-- "Owned N" on the recipe cards: fx counters ("refined:<item>", bound after the build), so
|
||||||
-- a collect's motes land there and the number counts up. Laid out at the final amount.
|
-- a collect's motes land there and the number counts up. Laid out at the final amount.
|
||||||
local counted = {}
|
local counted = {}
|
||||||
local function own_label(c, r)
|
local function own_label(c, r, color)
|
||||||
local l = fg.label(c, ui.money(r.owned), CAP_NUM, 24)
|
local l = fg.label(c, ui.money(r.owned or 0), { font = "mono_semi", px = 24, color = color }, 26)
|
||||||
counted[#counted + 1] = { key = "refined:" .. r.item, node = l.node, amount = r.owned }
|
counted[#counted + 1] = { key = "refined:" .. r.item, node = l.node, amount = r.owned or 0 }
|
||||||
return l
|
return l
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -501,8 +501,9 @@ local function recipe_card(self, H, c, st, rf, r)
|
|||||||
local why = (r.whyText or "") ~= "" and r.whyText or WHY[r.why] or r.why
|
local why = (r.whyText or "") ~= "" and r.whyText or WHY[r.why] or r.why
|
||||||
local owned = (r.owned or 0) > 0
|
local owned = (r.owned or 0) > 0
|
||||||
|
|
||||||
-- Head: the faceted crystal (.recipe-glyph: a 36 px pentagon, a faint rim), "Tier N ·
|
-- Head: the faceted crystal (.recipe-glyph: a 36 px pentagon, a faint rim), "Tier N" over
|
||||||
-- owned X" over the name.
|
-- the name, and on the right what you own, big, in the item's color (a collect's motes
|
||||||
|
-- fly into it and it counts up), shown at 0 too.
|
||||||
local glyph = { style = { width = 36, height = 36, shrink = 0 }, make = function(p, x, y)
|
local glyph = { style = { width = 36, height = 36, shrink = 0 }, make = function(p, x, y)
|
||||||
local g = ui.rect(p, x, y, 36, 36, color, 0.85)
|
local g = ui.rect(p, x, y, 36, 36, color, 0.85)
|
||||||
gui.play_flipbook(g, "pentagon")
|
gui.play_flipbook(g, "pentagon")
|
||||||
@@ -510,13 +511,14 @@ local function recipe_card(self, H, c, st, rf, r)
|
|||||||
end }
|
end }
|
||||||
local head = row({ height = 43.3, gap = 12, align = "start" }, {
|
local head = row({ height = 43.3, gap = 12, align = "start" }, {
|
||||||
col({ padding_top = 3.6 }, { glyph }),
|
col({ padding_top = 3.6 }, { glyph }),
|
||||||
col({ grow = 1, align = "start" }, {
|
col({ grow = 1, shrink = 1, align = "start" }, {
|
||||||
row({}, {
|
fg.label(c, "Tier " .. r.tier, "cap", 24),
|
||||||
fg.label(c, "Tier " .. r.tier .. (owned and " · owned " or ""), "cap", 24),
|
|
||||||
owned and own_label(c, r) or nil,
|
|
||||||
}),
|
|
||||||
fg.label(c, r.item, { font = "sans_semi", px = 16, color = "text", tracking = 0.14, upper = true }, 17.8, { fit = true }),
|
fg.label(c, r.item, { font = "sans_semi", px = 16, color = "text", tracking = 0.14, upper = true }, 17.8, { fit = true }),
|
||||||
}),
|
}),
|
||||||
|
col({ align = "end", shrink = 0 }, {
|
||||||
|
fg.label(c, "Owned", "cap", 14),
|
||||||
|
own_label(c, r, owned and color or "muted"),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Inputs per unit: need and name, what you hold on the right (red when short).
|
-- Inputs per unit: need and name, what you hold on the right (red when short).
|
||||||
@@ -558,32 +560,34 @@ local function recipe_card(self, H, c, st, rf, r)
|
|||||||
return n
|
return n
|
||||||
end })
|
end })
|
||||||
|
|
||||||
-- The form: quantity and Max on a row, Refine the card's width under them (beside the
|
-- The form: the quantity and Refine (that many) on a row, Max (as many as you can, the
|
||||||
-- quantity and "Max 250" it was cramped).
|
-- usual click) the card's width under them, primary.
|
||||||
local qty, input = qty_field(self, r, off, rf.maxBatch)
|
local qty, input = qty_field(self, r, off, rf.maxBatch)
|
||||||
qty.style.width, qty.style.grow = nil, 1
|
qty.style.width, qty.style.grow = nil, 1
|
||||||
local refine
|
local refine
|
||||||
local refine_btn = btn("Refine", BTN_SM, { primary = true, disabled = off }, function(b, nodes)
|
local refine_btn = btn("Refine", BTN_SM, { disabled = off }, function(b, nodes)
|
||||||
if off then
|
if off then
|
||||||
return tooltip.add(b, why)
|
return tooltip.add(b, why)
|
||||||
end
|
end
|
||||||
refine = H.action(self, b, nodes, "refine", function()
|
refine = H.action(self, b, nodes, "refine", function()
|
||||||
local q = self.refine_qty and self.refine_qty[r.item] or "1"
|
local q = self.refine_qty and self.refine_qty[r.item] or "1"
|
||||||
return { recipe = r.item, qty = (q ~= "" and q or "1") }
|
return { recipe = r.item, qty = (q ~= "" and q or "1") }
|
||||||
end)
|
end)
|
||||||
-- Enter in the quantity refines (the web's form).
|
tooltip.add(b, "Refine the number in the box")
|
||||||
blocks.push(self, "inputs", { input = input(), submit = refine, step = { min = 1, max = rf.maxBatch } })
|
-- Enter in the quantity refines (the web's form).
|
||||||
end, { grow = 1 })
|
blocks.push(self, "inputs", { input = input(), submit = refine, step = { min = 1, max = rf.maxBatch } })
|
||||||
local form = col({ shrink = 0 }, { row({ height = BTN_SM, gap = 6, shrink = 0 }, {
|
end)
|
||||||
qty,
|
local max_btn = btn(off and "Max" or ("Max · " .. ui.money(r.maxQty)), BTN_SM, { primary = true, disabled = off }, function(b, nodes)
|
||||||
btn(off and "Max" or ("Max " .. r.maxQty), BTN_SM, { disabled = off }, function(b, nodes)
|
if off then
|
||||||
if off then
|
return tooltip.add(b, why)
|
||||||
return tooltip.add(b, why)
|
end
|
||||||
end
|
H.action(self, b, nodes, "refine", { recipe = r.item, all = "1" })
|
||||||
H.action(self, b, nodes, "refine", { recipe = r.item, all = "1" })
|
tooltip.add(b, "Refine as many as you can (" .. ui.money(r.maxQty) .. ")")
|
||||||
tooltip.add(b, "Refine as many as you can (" .. ui.money(r.maxQty) .. ")")
|
end, { grow = 1 })
|
||||||
end),
|
local form = col({ shrink = 0 }, {
|
||||||
}), space(0, 6), row({ height = BTN_SM, shrink = 0 }, { refine_btn }) })
|
row({ height = BTN_SM, gap = 6, shrink = 0 }, { qty, refine_btn }),
|
||||||
|
space(0, 6), row({ height = BTN_SM, shrink = 0 }, { max_btn }),
|
||||||
|
})
|
||||||
|
|
||||||
local desc = catalog_desc(st, r.item)
|
local desc = catalog_desc(st, r.item)
|
||||||
return col({ padding = 17 }, {
|
return col({ padding = 17 }, {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Card sections (needs --fixture): a click on a tool card opens its drill panel, a click
|
"""Card sections (needs --fixture): a click on a tool row (Equipment) opens its drill panel, a click
|
||||||
on an advisor card shows it in the aside; hover lights a card's edge; seat tiles and the
|
on an advisor card shows it in the aside; hover lights a card's edge; seat tiles and the
|
||||||
close are focusable. Shots of each."""
|
close are focusable. Shots of each."""
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""The drill panel during onboarding (needs --fixture-file of a plain player:
|
"""The drill panel during onboarding (needs --fixture-file of a plain player:
|
||||||
scripts/fixture.py <api> --plain, and an MM_DEV=1 server): once the first deploy reveals
|
scripts/fixture.py <api> --plain, and an MM_DEV=1 server): once the first deploy reveals
|
||||||
Equipment, a tool card opens its panel (stats, the tracks' ladders) without the Store's
|
Equipment, a tool row opens its panel (stats, the tracks' ladders) without the Store's
|
||||||
parts (the upgrade buttons, Sell drill); after the first claim and sale reveal the Store,
|
parts (the upgrade buttons, Sell drill); after the first claim and sale reveal the Store,
|
||||||
the same panel has them. Shots "before" and "after", each linted."""
|
the same panel has them. Shots "before" and "after", each linted."""
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"""The Equipment table (needs --fixture): a row per tool, copies too (the fixture's two
|
||||||
|
PD-1s), one mining, one ready; the idle PD-1's cycle track taken to its top (Max in place
|
||||||
|
of its button) and the credits cut to a few hundred (some upgrades short). Shots of the
|
||||||
|
table, a row's drill panel, a row's Sell asking in place (Cancel on Sell's spot), and two
|
||||||
|
clicks on an affordable upgrade button (click_check: it stays put). Each linted. Args: an
|
||||||
|
optional name prefix."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
from lists import admin, call # noqa: E402
|
||||||
|
from sellask import ask, covers, rect_of # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def run(game, args):
|
||||||
|
fx = game.fixture
|
||||||
|
pre = (args[0] + "-") if args else ""
|
||||||
|
idle = fx["tool_idle"]
|
||||||
|
game.idle()
|
||||||
|
for _ in range(12):
|
||||||
|
r = call(game, "POST", "/api/act/upgrade", {"tool": idle, "track": "cycle"})
|
||||||
|
if not r.get("ok"):
|
||||||
|
break
|
||||||
|
st = call(game, "GET", "/api/state?s=equipment")["state"]
|
||||||
|
tr = [t for t in st["equipment"]["tools"] if t["id"] == idle][0]["tracks"][1]
|
||||||
|
game.expect(tr["next"] is None, "cycle track at its top: %s/%s" % (tr["level"], tr["max"]))
|
||||||
|
# One mining for sure (the fixture's stakes can find their well empty): an idle tool
|
||||||
|
# other than the PD-1s on the second moon.
|
||||||
|
ms = call(game, "GET", "/api/state?s=moons&moon=" + fx["moon2"])["state"]
|
||||||
|
wells = [w for w in ms["moon"]["wells"] if w["supply"] > 0]
|
||||||
|
spare = [t for t in st["equipment"]["tools"] if not t["stake"] and t["storeItemId"] != 1]
|
||||||
|
if wells and spare:
|
||||||
|
r = call(game, "POST", "/api/act/stake", {"tool": spare[0]["id"], "target": wells[0]["id"]})
|
||||||
|
game.expect(r.get("ok"), "stake: %s" % r.get("error"))
|
||||||
|
admin(game, "credits", amount=max(1, st["credits"] - 260), op="remove")
|
||||||
|
game.navigate("{section='equipment'}")
|
||||||
|
game.idle()
|
||||||
|
game.sleep(1)
|
||||||
|
game.shot(pre + "eqrows-table", settle=1.0)
|
||||||
|
game.lint(pre + "eqrows-table")
|
||||||
|
|
||||||
|
# A row opens its drill panel.
|
||||||
|
game.scroll_to("drill:" + idle)
|
||||||
|
game.idle()
|
||||||
|
game.click("drill:" + idle)
|
||||||
|
game.wait_for("view.drill == '%s' and not g.busy" % idle)
|
||||||
|
game.idle()
|
||||||
|
game.shot(pre + "eqrows-panel", settle=1.0)
|
||||||
|
game.lint(pre + "eqrows-panel")
|
||||||
|
|
||||||
|
# A row's Sell asks in place.
|
||||||
|
sell = "confirm:sell-tool:" + idle
|
||||||
|
game.scroll_to(sell)
|
||||||
|
game.idle()
|
||||||
|
before = rect_of(game, sell)
|
||||||
|
ask(game, sell)
|
||||||
|
game.expect(not game.eval("g.dialog:is_open()"), "no dialog opens")
|
||||||
|
game.expect(covers(rect_of(game, sell + ":cancel"), before), "Cancel takes Sell's spot")
|
||||||
|
game.shot(pre + "eqrows-asking", settle=0.3)
|
||||||
|
game.lint(pre + "eqrows-asking")
|
||||||
|
game.key("key_esc")
|
||||||
|
game.wait_for("g.ask == nil", 5)
|
||||||
|
game.idle()
|
||||||
|
|
||||||
|
# Two clicks on an affordable upgrade keep hitting the same row's button.
|
||||||
|
ups = [e for e in game.elements() if e["id"].startswith("act:upgrade?") and e.get("enabled", True)]
|
||||||
|
game.expect(ups, "an affordable upgrade")
|
||||||
|
if ups:
|
||||||
|
id_ = ups[0]["id"]
|
||||||
|
for n in range(2):
|
||||||
|
if game.eval("el(%r)" % id_):
|
||||||
|
game.click_check(id_, name=pre + "eqrows-click-%d" % n)
|
||||||
|
game.shot(pre + "eqrows-after", settle=1.0)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""Drill upgrade tracks (needs --fixture): the Equipment cards' two track boxes (yield and
|
"""Drill upgrade tracks (needs --fixture): the Equipment table's two track columns (yield and
|
||||||
cycle, side by side: level, value ▸ next, Upgrade or Max level, the kept need line) and the
|
cycle: value ▸ next, level pips, Upgrade or Max, the kept need line) and the
|
||||||
drill panel of the fixture's upgraded idle PD-1 (both boxes, both ladders, Sell drill).
|
drill panel of the fixture's upgraded idle PD-1 (both boxes, both ladders, Sell drill).
|
||||||
Clicking each Upgrade twice in the panel keeps the buttons where they are (click_check).
|
Clicking each Upgrade twice in the panel keeps the buttons where they are (click_check).
|
||||||
Shots: tracks-equipment, tracks-panel, tracks-panel-after. Args: an optional name prefix."""
|
Shots: tracks-equipment, tracks-panel, tracks-panel-after. Args: an optional name prefix."""
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"""Well cards (needs --fixture and an MM_DEV=1 server: admin finish-stakes): every action
|
"""Well cards (needs --fixture and an MM_DEV=1 server: admin finish-stakes): every action
|
||||||
on a well is on its card in the main panel, the aside only shows. Buys a fresh moon, then:
|
on a well is on its card in the main panel, the aside only shows. Buys a fresh moon, then:
|
||||||
the cards with their empty "+ Deploy" slots and the tray of idle drills (shot cards-empty);
|
the cards with their empty "+ Deploy" slots and the tray of idle drills (shot cards-empty);
|
||||||
picks a drill in the tray (cards-picked); deploys it twice from the same slot (the slot
|
a tray tile deploys straight to the selected well (cards-picked); the card's deploy slot
|
||||||
keeps its spot: click_check); a multi-well drill (the wide card); finishes the drills: the
|
keeps its spot (click_check); a multi-well drill (the wide card); finishes the drills: the
|
||||||
ready slots, gold with Claim and Redeploy (cards-ready); Claim on a slot (click_check), the
|
ready slots, gold with Claim and Redeploy (cards-ready); Claim on a slot (click_check), the
|
||||||
idle slot with Redrill (cards-idle), Redrill; selling the moon is in the main panel. The
|
idle slot with Redrill (cards-idle), Redrill; selling the moon is in the aside. The
|
||||||
aside, with a well and with none selected, has no buttons (aside-well, aside-moon).
|
aside, with a well and with none selected, has no buttons but Sell moon, the one destructive
|
||||||
|
action kept there (aside-well, aside-moon).
|
||||||
Args: an optional name prefix."""
|
Args: an optional name prefix."""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -26,7 +27,7 @@ def aside_buttons(game):
|
|||||||
r = e.get("rect")
|
r = e.get("rect")
|
||||||
if not r or not e.get("visible") or e.get("kind") == "wait":
|
if not r or not e.get("visible") or e.get("kind") == "wait":
|
||||||
continue
|
continue
|
||||||
if r["x"] >= left - 1 and r["y"] <= top + 1:
|
if r["x"] >= left - 1 and r["y"] <= top + 1 and not e["id"].startswith(("sell-moon", "moon-sale:")):
|
||||||
out.append(e["id"])
|
out.append(e["id"])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -52,21 +53,26 @@ def run(game, args):
|
|||||||
game.shot(pre + "cards-empty", settle=0.8)
|
game.shot(pre + "cards-empty", settle=0.8)
|
||||||
game.lint(pre + "cards-empty")
|
game.lint(pre + "cards-empty")
|
||||||
|
|
||||||
# Pick the last tile in the tray (one-well), deploy it from the first well's slot.
|
# A tray tile deploys straight to the selected well (no picking): select the first well,
|
||||||
tiles = ids(game, "pick:")
|
# click the lowest "stake" button there (the tray sits under the cards).
|
||||||
game.expect(len(tiles) > 0, "the tray has idle drills")
|
game.navigate("{section='moons', moon='%s', well='%s'}" % (moon["id"], wells[0]))
|
||||||
one = [t for t in tiles if t.endswith(":1")] # (reach 1; keys end with yield:cycle:reach)
|
|
||||||
pick = min(one or tiles, key=lambda t: float(t.split(":")[-3])) # (the smallest: room for two)
|
|
||||||
game.scroll_to(pick)
|
|
||||||
game.click(pick)
|
|
||||||
game.idle()
|
game.idle()
|
||||||
game.expect(game.eval("g.deploy_pick ~= nil"), "a tile picks")
|
|
||||||
game.shot(pre + "cards-picked", settle=0.5)
|
|
||||||
slot = "act:stake?target=" + wells[0]
|
slot = "act:stake?target=" + wells[0]
|
||||||
game.scroll_to(ids(game, slot)[0])
|
btns = [e for e in game.elements() if e["id"].startswith(slot) and e.get("visible", True)]
|
||||||
game.click_check(ids(game, slot)[0], name=pre + "click-deploy")
|
game.expect(len(btns) > 1, "the tray's tiles deploy to the selected well")
|
||||||
if ids(game, slot):
|
n0 = game.eval("#st.stakes")
|
||||||
game.click_check(ids(game, slot)[0], name=pre + "click-deploy-2")
|
tile = max(btns, key=lambda e: e["rect"]["y"])["id"]
|
||||||
|
game.scroll_to(tile)
|
||||||
|
game.click(tile)
|
||||||
|
game.wait_for("#st.stakes > %d" % n0, 10)
|
||||||
|
game.idle()
|
||||||
|
game.shot(pre + "cards-picked", settle=0.5)
|
||||||
|
# The card's deploy slot (topmost) keeps its spot under repeated clicks.
|
||||||
|
top = min((e for e in game.elements() if e["id"].startswith(slot) and e.get("visible", True)),
|
||||||
|
key=lambda e: e["rect"]["y"], default=None)
|
||||||
|
if top:
|
||||||
|
game.scroll_to(top["id"])
|
||||||
|
game.click_check(top["id"], name=pre + "click-deploy")
|
||||||
game.idle()
|
game.idle()
|
||||||
|
|
||||||
# A multi-well drill (bought when none is idle).
|
# A multi-well drill (bought when none is idle).
|
||||||
@@ -109,12 +115,14 @@ def run(game, args):
|
|||||||
game.shot(pre + "cards-idle", settle=0.8)
|
game.shot(pre + "cards-idle", settle=0.8)
|
||||||
redrill = ids(game, "act:redrill?redrill-well=" + wells[0])
|
redrill = ids(game, "act:redrill?redrill-well=" + wells[0])
|
||||||
left_here = game.eval("(function() for _, w in ipairs(st.moon.wells) do if w.id == '%s' then return w.supply end end end)()" % wells[0])
|
left_here = game.eval("(function() for _, w in ipairs(st.moon.wells) do if w.id == '%s' then return w.supply end end end)()" % wells[0])
|
||||||
game.expect(len(redrill) > 0 or not left_here, "the idle slot has Redrill (the well has %s left)" % left_here)
|
remain = game.eval("(function() for _, w in ipairs(st.moon.wells) do if w.id == '%s' then return w.remain end end end)()" % wells[0])
|
||||||
|
# (none while the drills on it take all it has: the server would refuse it)
|
||||||
|
game.expect(len(redrill) > 0 or not left_here or not remain, "the idle slot has Redrill (the well has %s left, %s free)" % (left_here, remain))
|
||||||
if redrill:
|
if redrill:
|
||||||
game.click(redrill[0])
|
game.click(redrill[0])
|
||||||
game.wait_for("not g.busy and #g.pending == 0", 20)
|
game.wait_for("not g.busy and #g.pending == 0", 20)
|
||||||
game.idle()
|
game.idle()
|
||||||
game.expect(ids(game, "moon-sale:" + moon["id"]) or ids(game, "sell-moon"), "selling the moon is in the main panel")
|
game.expect(ids(game, "moon-sale:" + moon["id"]) or ids(game, "sell-moon"), "selling the moon is in the aside")
|
||||||
game.shot(pre + "cards-redrilled", settle=0.8)
|
game.shot(pre + "cards-redrilled", settle=0.8)
|
||||||
left = aside_buttons(game)
|
left = aside_buttons(game)
|
||||||
game.expect(not left, "the aside (the moon) has no buttons: %s" % left)
|
game.expect(not left, "the aside (the moon) has no buttons: %s" % left)
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
# Wider steps (2026-10-22)
|
||||||
|
|
||||||
|
Playing the progression pass, the author had a LunarLite L100 (and money for more) about
|
||||||
|
33 minutes in, first C moon at 19 minutes: too fast. Their run's money: mostly raw sales
|
||||||
|
from five C moons, plus one Plasma Weave sale (₥32k, of which the refining margin was only
|
||||||
|
about ₥9k). The fix is a steeper grade ladder: each grade's price and wells, and its
|
||||||
|
drill's yield, price and upgrade prices, scaled by the same factor (C ×2, B ×3, A ×5,
|
||||||
|
S ×8), which keeps a moon's value (1.3–1.8× its price) and a drill's 4–5 claims per well on
|
||||||
|
its grade, while the jump to the next grade grows faster than income. D and the PD-1 are
|
||||||
|
unchanged. Refined items now gain about 20% over their inputs (was 40–100%).
|
||||||
|
|
||||||
|
Grades: D 300, C 12k, B 90k, A 1.5M, S 24M. Drills: PD-1 1,200 (8), PX-4 8,000 (120), L100
|
||||||
|
40,200 (900), L300 645k (20,000), LRE 9.44M (384k), EDE 17M (880k), LCE 24.8M (1.6M).
|
||||||
|
Sim, first moon of each grade: copies C 12m, B 30m, A 58m, S 2h33; boosts C 10m, B 21m,
|
||||||
|
A 40m, S 1h49 (was C 7m, B 15m, A 32m, S 1h04). A real player ran about 2.5× slower than
|
||||||
|
the sim's (C at 19m against the sim's 7m), so expect roughly C 30m, B 1h15 in play.
|
||||||
|
|
||||||
# Progression: each new drill rips through the current grade (2026-10-22)
|
# Progression: each new drill rips through the current grade (2026-10-22)
|
||||||
|
|
||||||
The loop: on a grade, its drill needs about 3–6 claims per well; the next drill (bought
|
The loop: on a grade, its drill needs about 3–6 claims per well; the next drill (bought
|
||||||
|
|||||||
+6
-6
@@ -85,8 +85,8 @@ The other sections' actions:
|
|||||||
| action | params | notes |
|
| action | params | notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `buy-tool` | `id` (store item id) | response has `buyModal` |
|
| `buy-tool` | `id` (store item id) | response has `buyModal` |
|
||||||
| `upgrade` | `tool` (tool id), `track` (`yield` or `cycle`) | one level on that track; `drill=` follows the copy upgraded |
|
| `upgrade` | `tool` (tool id), `track` (`yield` or `cycle`) | one level on that track of that tool (also while it mines: from its next drill) |
|
||||||
| `sell-tool` | `tool` | idle, nothing to claim, not the last tool (`noSale`) |
|
| `sell-tool` | `tool` | that tool: idle, nothing to claim, not the last tool (`noSale`) |
|
||||||
| `refine` | `recipe` (refined item name), `qty` or `all=1` | `all` makes as many as possible (`maxQty`) |
|
| `refine` | `recipe` (refined item name), `qty` or `all=1` | `all` makes as many as possible (`maxQty`) |
|
||||||
| `collect` | `job` (job id), or none for every finished batch | |
|
| `collect` | `job` (job id), or none for every finished batch | |
|
||||||
| `buy-slot` | – | refinery slot |
|
| `buy-slot` | – | refinery slot |
|
||||||
@@ -167,8 +167,8 @@ short code, listed with each field, and the web's text next to it.
|
|||||||
drill}
|
drill}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Copies of a tool (same type and track levels) are one row: `count` copies, `idle` of them
|
- Every tool is its own row, copies too: `count` is 1, `idle` 1 when it isn't mining, `stake`
|
||||||
idle; `id` is the copy shown (the selected one, else an idle one), `stake` its drill.
|
its drill.
|
||||||
- `tracks`: the tool's two upgrade tracks, yield then cycle: `{kind, name, level, max, value,
|
- `tracks`: the tool's two upgrade tracks, yield then cycle: `{kind, name, level, max, value,
|
||||||
next, ladder}`. `kind` is `yield` (`value`: the claim per cycle) or `cycle` (`value`: seconds
|
next, ladder}`. `kind` is `yield` (`value`: the claim per cycle) or `cycle` (`value`: seconds
|
||||||
per cycle); values are a stake's now, with the player's bonuses and live events. Upgrade
|
per cycle); values are a stake's now, with the player's bonuses and live events. Upgrade
|
||||||
@@ -352,8 +352,8 @@ The client may show results before the server answers; the next state replaces t
|
|||||||
recompute: the moon's stakes, oldest `start` first (ties in `stakes` order), are claimed
|
recompute: the moon's stakes, oldest `start` first (ties in `stakes` order), are claimed
|
||||||
in turn, each from what the earlier ones left (`remain`, starting at `supply`). A one-well
|
in turn, each from what the earlier ones left (`remain`, starting at `supply`). A one-well
|
||||||
stake takes `min(amount, remain)` of its well; a multi-well stake (`reach` > 1) covers its
|
stake takes `min(amount, remain)` of its well; a multi-well stake (`reach` > 1) covers its
|
||||||
own well (if not empty) then the others with something left, most first (ties in
|
own well (if not empty) then the next wells in `moon.wells` order (wrapping round), each
|
||||||
`moon.wells` order), up to `reach` wells, taking from each in turn as much as is left
|
if it has something left, up to `reach` wells, taking from each in turn as much as is left
|
||||||
until it has `amount`. `demand` is the one-well amounts on the well plus what each
|
until it has `amount`. `demand` is the one-well amounts on the well plus what each
|
||||||
multi-well stake would take from it out of `supply`; `dryAfter` the claim (among those
|
multi-well stake would take from it out of `supply`; `dryAfter` the claim (among those
|
||||||
taking from the well) that leaves it at 0. A deploy card's `here` is what a new stake
|
taking from the well) that leaves it at 0. A deploy card's `here` is what a new stake
|
||||||
|
|||||||
+89
-89
@@ -5,13 +5,13 @@
|
|||||||
"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": 23, "wellSupplyMax": 46, "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": 6000, "minValue": 1.3, "wellSupplyMin": 216, "wellSupplyMax": 430, "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}},
|
||||||
{"grade": "B", "price": 30000, "wellSupplyMin": 738, "wellSupplyMax": 1968, "wellCountWeights": {"2": 30, "3": 40, "4": 30},
|
{"grade": "B", "price": 90000, "wellSupplyMin": 2214, "wellSupplyMax": 5904, "wellCountWeights": {"2": 30, "3": 40, "4": 30},
|
||||||
"affinities": {"Sollux": 1, "Shadowstone": 1.5, "Azurium": 2, "Novafor": 2, "Nebulance": 1.2}},
|
"affinities": {"Sollux": 1, "Shadowstone": 1.5, "Azurium": 2, "Novafor": 2, "Nebulance": 1.2}},
|
||||||
{"grade": "A", "price": 300000, "wellSupplyMin": 4920, "wellSupplyMax": 12915, "wellCountWeights": {"3": 30, "4": 40, "5": 30},
|
{"grade": "A", "price": 1500000, "wellSupplyMin": 24600, "wellSupplyMax": 64575, "wellCountWeights": {"3": 30, "4": 40, "5": 30},
|
||||||
"affinities": {"Sollux": 0.6, "Shadowstone": 1, "Azurium": 1.5, "Novafor": 2, "Nebulance": 2}},
|
"affinities": {"Sollux": 0.6, "Shadowstone": 1, "Azurium": 1.5, "Novafor": 2, "Nebulance": 2}},
|
||||||
{"grade": "S", "price": 3000000, "wellSupplyMin": 42230, "wellSupplyMax": 103730, "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}}
|
||||||
],
|
],
|
||||||
"moonResale": {"factor": 0.08, "maxRatio": 0.9, "minHoldSecs": 600},
|
"moonResale": {"factor": 0.08, "maxRatio": 0.9, "minHoldSecs": 600},
|
||||||
@@ -55,23 +55,23 @@
|
|||||||
"name": "Percussion Drill PX-4",
|
"name": "Percussion Drill PX-4",
|
||||||
"description": "Hammers the crust a thousand times a second to crack open ore seams.",
|
"description": "Hammers the crust a thousand times a second to crack open ore seams.",
|
||||||
"image": "pickaxe.png",
|
"image": "pickaxe.png",
|
||||||
"price": 4000,
|
"price": 8000,
|
||||||
"claimAmount": 60,
|
"claimAmount": 120,
|
||||||
"completionTimeInSecs": 6,
|
"completionTimeInSecs": 6,
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"yield": {"name": "Wider chisel", "levels": [
|
"yield": {"name": "Wider chisel", "levels": [
|
||||||
{"value": 72, "price": 760},
|
{"value": 144, "price": 1520},
|
||||||
{"value": 84, "price": 760},
|
{"value": 168, "price": 1520},
|
||||||
{"value": 96, "price": 760, "parts": {"Dusk Alloy": 2}},
|
{"value": 192, "price": 1520, "parts": {"Dusk Alloy": 2}},
|
||||||
{"value": 108, "price": 760, "parts": {"Cryo Cell": 1}},
|
{"value": 216, "price": 1520, "parts": {"Cryo Cell": 1}},
|
||||||
{"value": 120, "price": 760, "parts": {"Plasma Weave": 1}}
|
{"value": 240, "price": 1520, "parts": {"Plasma Weave": 1}}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Pneumatic hammer", "levels": [
|
"cycle": {"name": "Pneumatic hammer", "levels": [
|
||||||
{"value": 5.2, "price": 580},
|
{"value": 5.2, "price": 1160},
|
||||||
{"value": 4.5, "price": 680},
|
{"value": 4.5, "price": 1360},
|
||||||
{"value": 3.9, "price": 780, "parts": {"Dusk Alloy": 2}},
|
{"value": 3.9, "price": 1560, "parts": {"Dusk Alloy": 2}},
|
||||||
{"value": 3.4, "price": 860, "parts": {"Cryo Cell": 1}},
|
{"value": 3.4, "price": 1720, "parts": {"Cryo Cell": 1}},
|
||||||
{"value": 3, "price": 890, "parts": {"Plasma Weave": 1}}
|
{"value": 3, "price": 1780, "parts": {"Plasma Weave": 1}}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -80,23 +80,23 @@
|
|||||||
"name": "LunarLite L100",
|
"name": "LunarLite L100",
|
||||||
"description": "An entry-level rotary drill: point it down and the ore rolls in.",
|
"description": "An entry-level rotary drill: point it down and the ore rolls in.",
|
||||||
"image": "lunarlite.jpg",
|
"image": "lunarlite.jpg",
|
||||||
"price": 13400,
|
"price": 40200,
|
||||||
"claimAmount": 300,
|
"claimAmount": 900,
|
||||||
"completionTimeInSecs": 8,
|
"completionTimeInSecs": 8,
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"yield": {"name": "Wide-bore bit", "levels": [
|
"yield": {"name": "Wide-bore bit", "levels": [
|
||||||
{"value": 355, "price": 2300},
|
{"value": 1060, "price": 6900},
|
||||||
{"value": 423, "price": 2900},
|
{"value": 1270, "price": 8700},
|
||||||
{"value": 477, "price": 2300, "parts": {"Cryo Cell": 1}},
|
{"value": 1430, "price": 6900, "parts": {"Cryo Cell": 1}},
|
||||||
{"value": 545, "price": 2900, "parts": {"Plasma Weave": 2}},
|
{"value": 1640, "price": 8700, "parts": {"Plasma Weave": 2}},
|
||||||
{"value": 600, "price": 2300, "parts": {"Plasma Weave": 3}}
|
{"value": 1800, "price": 6900, "parts": {"Plasma Weave": 3}}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Spindle motor", "levels": [
|
"cycle": {"name": "Spindle motor", "levels": [
|
||||||
{"value": 7, "price": 1800},
|
{"value": 7, "price": 5400},
|
||||||
{"value": 6, "price": 2400},
|
{"value": 6, "price": 7200},
|
||||||
{"value": 5.2, "price": 2600, "parts": {"Cryo Cell": 1}},
|
{"value": 5.2, "price": 7800, "parts": {"Cryo Cell": 1}},
|
||||||
{"value": 4.6, "price": 2600, "parts": {"Plasma Weave": 2}},
|
{"value": 4.6, "price": 7800, "parts": {"Plasma Weave": 2}},
|
||||||
{"value": 4, "price": 3300, "parts": {"Plasma Weave": 3}}
|
{"value": 4, "price": 9900, "parts": {"Plasma Weave": 3}}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -105,24 +105,24 @@
|
|||||||
"name": "LunarLite L300",
|
"name": "LunarLite L300",
|
||||||
"description": "The L100's laser-cutting big brother, built to burn through bedrock.",
|
"description": "The L100's laser-cutting big brother, built to burn through bedrock.",
|
||||||
"image": "lunarlite300.png",
|
"image": "lunarlite300.png",
|
||||||
"price": 129000,
|
"price": 645000,
|
||||||
"claimAmount": 4000,
|
"claimAmount": 20000,
|
||||||
"completionTimeInSecs": 10,
|
"completionTimeInSecs": 10,
|
||||||
"reach": 2,
|
"reach": 2,
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"yield": {"name": "Wider beam", "levels": [
|
"yield": {"name": "Wider beam", "levels": [
|
||||||
{"value": 4740, "price": 23000},
|
{"value": 23700, "price": 115000},
|
||||||
{"value": 5630, "price": 27000},
|
{"value": 28200, "price": 135000},
|
||||||
{"value": 6370, "price": 23000, "parts": {"Cryo Cell": 2}},
|
{"value": 31800, "price": 115000, "parts": {"Cryo Cell": 2}},
|
||||||
{"value": 7260, "price": 27000, "parts": {"Plasma Weave": 3}},
|
{"value": 36300, "price": 135000, "parts": {"Plasma Weave": 3}},
|
||||||
{"value": 8000, "price": 23000, "parts": {"Void Core": 1}}
|
{"value": 40000, "price": 115000, "parts": {"Void Core": 1}}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Laser capacitors", "levels": [
|
"cycle": {"name": "Laser capacitors", "levels": [
|
||||||
{"value": 8.7, "price": 18000},
|
{"value": 8.7, "price": 90000},
|
||||||
{"value": 7.5, "price": 23000},
|
{"value": 7.5, "price": 115000},
|
||||||
{"value": 6.5, "price": 25000, "parts": {"Cryo Cell": 2}},
|
{"value": 6.5, "price": 125000, "parts": {"Cryo Cell": 2}},
|
||||||
{"value": 5.7, "price": 26000, "parts": {"Plasma Weave": 3}},
|
{"value": 5.7, "price": 130000, "parts": {"Plasma Weave": 3}},
|
||||||
{"value": 5, "price": 30000, "parts": {"Void Core": 1}}
|
{"value": 5, "price": 150000, "parts": {"Void Core": 1}}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -131,24 +131,24 @@
|
|||||||
"name": "Lunar Regolith Extractor LRE-3000",
|
"name": "Lunar Regolith Extractor LRE-3000",
|
||||||
"description": "An auger rig that chews through regolith by the ton and keeps every grain worth selling.",
|
"description": "An auger rig that chews through regolith by the ton and keeps every grain worth selling.",
|
||||||
"image": "regolith.jpg",
|
"image": "regolith.jpg",
|
||||||
"price": 1180000,
|
"price": 9440000,
|
||||||
"claimAmount": 48000,
|
"claimAmount": 384000,
|
||||||
"completionTimeInSecs": 12,
|
"completionTimeInSecs": 12,
|
||||||
"reach": 3,
|
"reach": 3,
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"yield": {"name": "Fine sieves", "levels": [
|
"yield": {"name": "Fine sieves", "levels": [
|
||||||
{"value": 56470, "price": 200000},
|
{"value": 452000, "price": 1600000},
|
||||||
{"value": 67760, "price": 260000},
|
{"value": 542000, "price": 2080000},
|
||||||
{"value": 76240, "price": 200000, "parts": {"Plasma Weave": 3}},
|
{"value": 610000, "price": 1600000, "parts": {"Plasma Weave": 3}},
|
||||||
{"value": 87530, "price": 260000, "parts": {"Void Core": 1}},
|
{"value": 700000, "price": 2080000, "parts": {"Void Core": 1}},
|
||||||
{"value": 96000, "price": 200000, "parts": {"Stellar Lattice": 1}}
|
{"value": 768000, "price": 1600000, "parts": {"Stellar Lattice": 1}}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Auger gearbox", "levels": [
|
"cycle": {"name": "Auger gearbox", "levels": [
|
||||||
{"value": 10.4, "price": 170000},
|
{"value": 10.4, "price": 1360000},
|
||||||
{"value": 9, "price": 200000},
|
{"value": 9, "price": 1600000},
|
||||||
{"value": 7.8, "price": 230000, "parts": {"Plasma Weave": 3}},
|
{"value": 7.8, "price": 1840000, "parts": {"Plasma Weave": 3}},
|
||||||
{"value": 6.8, "price": 250000, "parts": {"Void Core": 1}},
|
{"value": 6.8, "price": 2000000, "parts": {"Void Core": 1}},
|
||||||
{"value": 6, "price": 260000, "parts": {"Stellar Lattice": 1}}
|
{"value": 6, "price": 2080000, "parts": {"Stellar Lattice": 1}}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -157,24 +157,24 @@
|
|||||||
"name": "Electrostatic Dust Excavator EDE-700",
|
"name": "Electrostatic Dust Excavator EDE-700",
|
||||||
"description": "Charged plates pull ore-rich dust straight out of the ground, no digging needed.",
|
"description": "Charged plates pull ore-rich dust straight out of the ground, no digging needed.",
|
||||||
"image": "dustexcavator.png",
|
"image": "dustexcavator.png",
|
||||||
"price": 2120000,
|
"price": 17000000,
|
||||||
"claimAmount": 110000,
|
"claimAmount": 880000,
|
||||||
"completionTimeInSecs": 14,
|
"completionTimeInSecs": 14,
|
||||||
"reach": 5,
|
"reach": 5,
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"yield": {"name": "Larger plates", "levels": [
|
"yield": {"name": "Larger plates", "levels": [
|
||||||
{"value": 131390, "price": 390000},
|
{"value": 1050000, "price": 3120000},
|
||||||
{"value": 152780, "price": 390000},
|
{"value": 1220000, "price": 3120000},
|
||||||
{"value": 177220, "price": 450000, "parts": {"Void Core": 1}},
|
{"value": 1420000, "price": 3600000, "parts": {"Void Core": 1}},
|
||||||
{"value": 198610, "price": 390000, "parts": {"Stellar Lattice": 1}},
|
{"value": 1590000, "price": 3120000, "parts": {"Stellar Lattice": 1}},
|
||||||
{"value": 220000, "price": 390000, "parts": {"Void Core": 2, "Stellar Lattice": 2}}
|
{"value": 1760000, "price": 3120000, "parts": {"Void Core": 2, "Stellar Lattice": 2}}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Charge coils", "levels": [
|
"cycle": {"name": "Charge coils", "levels": [
|
||||||
{"value": 12.2, "price": 300000},
|
{"value": 12.2, "price": 2400000},
|
||||||
{"value": 10.5, "price": 370000},
|
{"value": 10.5, "price": 2960000},
|
||||||
{"value": 9.1, "price": 410000, "parts": {"Void Core": 1}},
|
{"value": 9.1, "price": 3280000, "parts": {"Void Core": 1}},
|
||||||
{"value": 8, "price": 430000, "parts": {"Stellar Lattice": 1}},
|
{"value": 8, "price": 3440000, "parts": {"Stellar Lattice": 1}},
|
||||||
{"value": 7, "price": 500000, "parts": {"Void Core": 2, "Stellar Lattice": 2}}
|
{"value": 7, "price": 4000000, "parts": {"Void Core": 2, "Stellar Lattice": 2}}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -183,24 +183,24 @@
|
|||||||
"name": "Lunar Core Extractor LCE-360",
|
"name": "Lunar Core Extractor LCE-360",
|
||||||
"description": "A deep-core rig that bores to the mantle and hauls up what's been buried for eons.",
|
"description": "A deep-core rig that bores to the mantle and hauls up what's been buried for eons.",
|
||||||
"image": "lunarcoreextractor.png",
|
"image": "lunarcoreextractor.png",
|
||||||
"price": 3100000,
|
"price": 24800000,
|
||||||
"claimAmount": 200000,
|
"claimAmount": 1600000,
|
||||||
"completionTimeInSecs": 16,
|
"completionTimeInSecs": 16,
|
||||||
"reach": 5,
|
"reach": 5,
|
||||||
"tracks": {
|
"tracks": {
|
||||||
"yield": {"name": "Core barrel", "levels": [
|
"yield": {"name": "Core barrel", "levels": [
|
||||||
{"value": 238300, "price": 560000},
|
{"value": 1910000, "price": 4480000},
|
||||||
{"value": 280850, "price": 630000},
|
{"value": 2250000, "price": 5040000},
|
||||||
{"value": 319150, "price": 560000, "parts": {"Void Core": 2}},
|
{"value": 2550000, "price": 4480000, "parts": {"Void Core": 2}},
|
||||||
{"value": 361700, "price": 630000, "parts": {"Stellar Lattice": 2}},
|
{"value": 2890000, "price": 5040000, "parts": {"Stellar Lattice": 2}},
|
||||||
{"value": 400000, "price": 560000, "parts": {"Void Core": 3, "Stellar Lattice": 3}}
|
{"value": 3200000, "price": 4480000, "parts": {"Void Core": 3, "Stellar Lattice": 3}}
|
||||||
]},
|
]},
|
||||||
"cycle": {"name": "Drill string", "levels": [
|
"cycle": {"name": "Drill string", "levels": [
|
||||||
{"value": 14, "price": 420000},
|
{"value": 14, "price": 3360000},
|
||||||
{"value": 12, "price": 560000},
|
{"value": 12, "price": 4480000},
|
||||||
{"value": 10.4, "price": 600000, "parts": {"Void Core": 2}},
|
{"value": 10.4, "price": 4800000, "parts": {"Void Core": 2}},
|
||||||
{"value": 9.1, "price": 650000, "parts": {"Stellar Lattice": 2}},
|
{"value": 9.1, "price": 5200000, "parts": {"Stellar Lattice": 2}},
|
||||||
{"value": 8, "price": 710000, "parts": {"Void Core": 3, "Stellar Lattice": 3}}
|
{"value": 8, "price": 5680000, "parts": {"Void Core": 3, "Stellar Lattice": 3}}
|
||||||
]}
|
]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,31 +257,31 @@
|
|||||||
},
|
},
|
||||||
"refinery": {
|
"refinery": {
|
||||||
"slotPrices": [1500, 4000, 10000, 25000, 60000, 150000, 400000, 1000000, 2500000],
|
"slotPrices": [1500, 4000, 10000, 25000, 60000, 150000, 400000, 1000000, 2500000],
|
||||||
"maxBatch": 25,
|
"maxBatch": 100,
|
||||||
"batchUpgrades": [
|
"batchUpgrades": [
|
||||||
{ "maxBatch": 50, "price": 6000 },
|
{ "maxBatch": 250, "price": 6000 },
|
||||||
{ "maxBatch": 100, "price": 25000 },
|
{ "maxBatch": 500, "price": 30000 },
|
||||||
{ "maxBatch": 250, "price": 120000 },
|
{ "maxBatch": 1000, "price": 150000 },
|
||||||
{ "maxBatch": 500, "price": 600000 },
|
{ "maxBatch": 2500, "price": 600000 },
|
||||||
{ "maxBatch": 1000, "price": 3000000 },
|
{ "maxBatch": 5000, "price": 3000000 },
|
||||||
{ "maxBatch": 2500, "price": 15000000 }
|
{ "maxBatch": 10000, "price": 15000000 }
|
||||||
],
|
],
|
||||||
"bulkLog": 1.83,
|
"bulkLog": 1.0,
|
||||||
"unlockGrade": "C",
|
"unlockGrade": "C",
|
||||||
"items": [
|
"items": [
|
||||||
{"name": "Dusk Alloy", "tier": 1, "color": "#818cf8", "price": 40, "seconds": 1,
|
{"name": "Dusk Alloy", "tier": 1, "color": "#818cf8", "price": 24, "seconds": 1,
|
||||||
"inputs": {"Sollux": 2, "Shadowstone": 2},
|
"inputs": {"Sollux": 2, "Shadowstone": 2},
|
||||||
"description": "Sunlit Sollux fused into Shadowstone: light, hard plating for drill housings."},
|
"description": "Sunlit Sollux fused into Shadowstone: light, hard plating for drill housings."},
|
||||||
{"name": "Cryo Cell", "tier": 1, "color": "#67e8f9", "price": 113, "seconds": 2,
|
{"name": "Cryo Cell", "tier": 1, "color": "#67e8f9", "price": 97, "seconds": 2,
|
||||||
"inputs": {"Azurium": 3, "Novafor": 3},
|
"inputs": {"Azurium": 3, "Novafor": 3},
|
||||||
"description": "Azurium coolant sealed around a Novafor core keeps bits from melting at depth."},
|
"description": "Azurium coolant sealed around a Novafor core keeps bits from melting at depth."},
|
||||||
{"name": "Plasma Weave", "tier": 1, "color": "#f472b6", "price": 157, "seconds": 3,
|
{"name": "Plasma Weave", "tier": 1, "color": "#f472b6", "price": 134, "seconds": 2,
|
||||||
"inputs": {"Nebulance": 4, "Sollux": 2},
|
"inputs": {"Nebulance": 4, "Sollux": 2},
|
||||||
"description": "Charged Nebulance spun on a Sollux lattice, the conductive mesh in every cutting head."},
|
"description": "Charged Nebulance spun on a Sollux lattice, the conductive mesh in every cutting head."},
|
||||||
{"name": "Void Core", "tier": 2, "color": "#c4b5fd", "price": 580, "seconds": 5,
|
{"name": "Void Core", "tier": 2, "unlockGrade": "B", "color": "#c4b5fd", "price": 450, "seconds": 3,
|
||||||
"inputs": {"Dusk Alloy": 2, "Cryo Cell": 2, "Nebulance": 5},
|
"inputs": {"Dusk Alloy": 2, "Cryo Cell": 2, "Nebulance": 5},
|
||||||
"description": "A cold, dense power core that drives the heaviest rigs for weeks."},
|
"description": "A cold, dense power core that drives the heaviest rigs for weeks."},
|
||||||
{"name": "Stellar Lattice", "tier": 2, "color": "#fde047", "price": 630, "seconds": 6,
|
{"name": "Stellar Lattice", "tier": 2, "unlockGrade": "B", "color": "#fde047", "price": 470, "seconds": 3,
|
||||||
"inputs": {"Plasma Weave": 2, "Cryo Cell": 1, "Shadowstone": 4},
|
"inputs": {"Plasma Weave": 2, "Cryo Cell": 1, "Shadowstone": 4},
|
||||||
"description": "A self-aligning crystal frame that lets deep-core extractors bore without drift."}
|
"description": "A self-aligning crystal frame that lets deep-core extractors bore without drift."}
|
||||||
]
|
]
|
||||||
|
|||||||
+37
-18
@@ -1017,13 +1017,31 @@ func stakeInTx(ctx context.Context, tx pgx.Tx, userID, toolID, wellID string) er
|
|||||||
// on an empty well it goes to the moon's fullest one instead (Redrill, redeploy).
|
// on an empty well it goes to the moon's fullest one instead (Redrill, redeploy).
|
||||||
reach := item.ToolReach()
|
reach := item.ToolReach()
|
||||||
if supply <= 0 && reach > 1 {
|
if supply <= 0 && reach > 1 {
|
||||||
err = tx.QueryRow(ctx, `
|
// Its well is empty: the next one with supply, in the moon's order (wrapping round),
|
||||||
select id, supply, (select name from resource where id = resource_id) from resource_well
|
// as coverWells walks them.
|
||||||
where source_id = $1 and supply > 0 order by supply desc, id limit 1`,
|
rows, err := tx.Query(ctx, `
|
||||||
moonID).Scan(&wellID, &supply, &resource)
|
select resource_well.id, resource_well.supply, resource.name
|
||||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
from resource_well join resource on resource.id = resource_well.resource_id
|
||||||
|
where source_id = $1 order by resource.name, resource_well.id`, moonID)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
type w struct {
|
||||||
|
ID string
|
||||||
|
Supply float64
|
||||||
|
Resource string
|
||||||
|
}
|
||||||
|
wells, err := pgx.CollectRows(rows, pgx.RowToStructByPos[w])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
at := slices.IndexFunc(wells, func(x w) bool { return x.ID == wellID })
|
||||||
|
for k := 1; k <= len(wells); k++ {
|
||||||
|
if x := wells[(max(at, 0)+k)%len(wells)]; x.Supply > 0 {
|
||||||
|
wellID, supply, resource = x.ID, x.Supply, x.Resource
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if supply <= 0 {
|
if supply <= 0 {
|
||||||
return errWellEmpty
|
return errWellEmpty
|
||||||
@@ -1325,22 +1343,23 @@ func redrillPick(amounts []float64, left float64) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// coverWells picks the wells (indexes into supply) a stake with the given reach mines:
|
// coverWells picks the wells (indexes into supply) a stake with the given reach mines:
|
||||||
// the one it was deployed on (anchor; -1 for none) first, then the others richest first,
|
// the one it was deployed on (anchor; -1 for none) first, then its neighbours in the moon's
|
||||||
// leaving out empty ones, up to reach wells.
|
// order (the next well, wrapping round), leaving out empty ones (in a projection, those
|
||||||
|
// the earlier claims empty: a well about to be tapped out is passed over), up to reach.
|
||||||
func coverWells(anchor int, supply []float64, reach int) []int {
|
func coverWells(anchor int, supply []float64, reach int) []int {
|
||||||
var others []int
|
n := len(supply)
|
||||||
for i, s := range supply {
|
start := anchor
|
||||||
if i != anchor && s > 0 {
|
if start < 0 || start >= n {
|
||||||
others = append(others, i)
|
start = 0
|
||||||
|
}
|
||||||
|
var out []int
|
||||||
|
for k := 0; k < n && len(out) < max(1, reach); k++ {
|
||||||
|
i := (start + k) % n
|
||||||
|
if supply[i] > 0 {
|
||||||
|
out = append(out, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
slices.SortStableFunc(others, func(a, b int) int { return cmpDesc(supply[a], supply[b]) })
|
return out
|
||||||
var out []int
|
|
||||||
if anchor >= 0 && anchor < len(supply) && supply[anchor] > 0 {
|
|
||||||
out = append(out, anchor)
|
|
||||||
}
|
|
||||||
out = append(out, others...)
|
|
||||||
return out[:min(len(out), max(1, reach))]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitClaim is what a claim of amount takes from each well (indexes as in supply): from
|
// splitClaim is what a claim of amount takes from each well (indexes as in supply): from
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ package main
|
|||||||
// - a gate passed: equipment (first deploy), market (first claim), store (first sale),
|
// - a gate passed: equipment (first deploy), market (first claim), store (first sale),
|
||||||
// refinery (bought a moon of refinery.unlockGrade or higher), advisors (bought a tool);
|
// refinery (bought a moon of refinery.unlockGrade or higher), advisors (bought a tool);
|
||||||
// - claimed:<resource>, the resources claimed so far;
|
// - claimed:<resource>, the resources claimed so far;
|
||||||
|
// - grade:<G>, the moon grades bought so far (recipes with an unlockGrade, refinery.go);
|
||||||
// - seen:<tab>, the revealed tab was opened (no more shine);
|
// - seen:<tab>, the revealed tab was opened (no more shine);
|
||||||
// - all: everything unlocked (players from before onboarding, admin "unlock all").
|
// - all: everything unlocked (players from before onboarding, admin "unlock all").
|
||||||
// Owning a moon is read from the game state. Redrill opens with the first claim. The store gate also reveals "+ New moon",
|
// Owning a moon is read from the game state. Redrill opens with the first claim. The store gate also reveals "+ New moon",
|
||||||
@@ -101,14 +102,14 @@ func unlockClaim(ctx context.Context, tx pgx.Tx, userID, resource string) error
|
|||||||
return unlock(ctx, tx, userID, "market", "claimed:"+resource)
|
return unlock(ctx, tx, userID, "market", "claimed:"+resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
// unlockRefineryAt opens the refinery when the player buys a moon of refinery.unlockGrade
|
// unlockRefineryAt records the grade of a moon bought (grade:<G>, for recipes with an
|
||||||
// or higher.
|
// unlockGrade: recipeOpen) and opens the refinery at refinery.unlockGrade or higher.
|
||||||
func unlockRefineryAt(ctx context.Context, tx pgx.Tx, userID, grade string) error {
|
func unlockRefineryAt(ctx context.Context, tx pgx.Tx, userID, grade string) error {
|
||||||
c := cfg()
|
c := cfg()
|
||||||
if c.gradeIndex(grade) < c.gradeIndex(c.Refinery.UnlockGrade) {
|
if c.gradeIndex(grade) < c.gradeIndex(c.Refinery.UnlockGrade) {
|
||||||
return nil
|
return unlock(ctx, tx, userID, "grade:"+grade)
|
||||||
}
|
}
|
||||||
return unlock(ctx, tx, userID, "refinery")
|
return unlock(ctx, tx, userID, "refinery", "grade:"+grade)
|
||||||
}
|
}
|
||||||
|
|
||||||
// markSeen clears the shine on a revealed tab once the player opens it.
|
// markSeen clears the shine on a revealed tab once the player opens it.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -52,7 +53,7 @@ func TestOverdrillSingle(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDrillsOnWideWell(t *testing.T) {
|
func TestDrillsOnWideWell(t *testing.T) {
|
||||||
// A whole-moon drill on w0 (12 of 10 + 5 + 4) takes w0 then the richest (w1).
|
// A whole-moon drill on w0 (12 of 10 + 5 + 4) takes w0 then the next well (w1).
|
||||||
m := odMoon(10, 5, 4)
|
m := odMoon(10, 5, 4)
|
||||||
stakes := []Stake{odStake(1, "w0", 12, 3)}
|
stakes := []Stake{odStake(1, "w0", 12, 3)}
|
||||||
s := &State{Stakes: stakes}
|
s := &State{Stakes: stakes}
|
||||||
@@ -148,3 +149,24 @@ func TestWideTieOrder(t *testing.T) {
|
|||||||
t.Fatalf("tie split %v", got)
|
t.Fatalf("tie split %v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A multi-well drill mines its own well, then the next ones in the moon's order (not the
|
||||||
|
// richest), passing over empty ones and wrapping round.
|
||||||
|
func TestCoverWellsNext(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
anchor int
|
||||||
|
supply []float64
|
||||||
|
reach int
|
||||||
|
want []int
|
||||||
|
}{
|
||||||
|
{0, []float64{10, 5, 90, 4}, 2, []int{0, 1}},
|
||||||
|
{1, []float64{10, 0, 5, 90}, 3, []int{2, 3, 0}},
|
||||||
|
{2, []float64{10, 5, 7, 0}, 3, []int{2, 0, 1}},
|
||||||
|
{3, []float64{10, 5, 7, 4}, 2, []int{3, 0}},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := coverWells(c.anchor, c.supply, c.reach); !slices.Equal(got, c.want) {
|
||||||
|
t.Errorf("coverWells(%d, %v, %d) = %v, want %v", c.anchor, c.supply, c.reach, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Void Core and Stellar Lattice open with the first B moon (or higher, or "all").
|
||||||
|
func TestRecipeOpen(t *testing.T) {
|
||||||
|
item := cfg().refinedItem("Void Core")
|
||||||
|
if item == nil || item.UnlockGrade != "B" {
|
||||||
|
t.Fatalf("Void Core should open at B: %+v", item)
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
flags []string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{nil, false},
|
||||||
|
{[]string{"refinery", "grade:D", "grade:C"}, false},
|
||||||
|
{[]string{"grade:B"}, true},
|
||||||
|
{[]string{"grade:S"}, true},
|
||||||
|
{[]string{"all"}, true},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := recipeOpen(item, c.flags); got != c.want {
|
||||||
|
t.Errorf("recipeOpen(Void Core, %v) = %v, want %v", c.flags, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !recipeOpen(cfg().refinedItem("Dusk Alloy"), nil) {
|
||||||
|
t.Error("Dusk Alloy has no unlock grade: open from the start")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
@@ -91,6 +93,24 @@ type RefinedItem struct {
|
|||||||
Seconds int `json:"seconds"`
|
Seconds int `json:"seconds"`
|
||||||
Inputs map[string]float64 `json:"inputs"`
|
Inputs map[string]float64 `json:"inputs"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
// The recipe shows (and can be refined) once the player has bought a moon of this grade
|
||||||
|
// or higher (onboarding flag grade:<G>, recipeOpen); "" from the start.
|
||||||
|
UnlockGrade string `json:"unlockGrade,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// recipeOpen says whether a recipe is available to a player with these onboarding flags.
|
||||||
|
func recipeOpen(item *RefinedItem, flags []string) bool {
|
||||||
|
if item.UnlockGrade == "" || slices.Contains(flags, "all") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
c := cfg()
|
||||||
|
want := c.gradeIndex(item.UnlockGrade)
|
||||||
|
for _, f := range flags {
|
||||||
|
if g, ok := strings.CutPrefix(f, "grade:"); ok && c.gradeIndex(g) >= want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GameConfig) refinedItem(name string) *RefinedItem {
|
func (c *GameConfig) refinedItem(name string) *RefinedItem {
|
||||||
@@ -115,6 +135,11 @@ func (c *GameConfig) unitPrice(name string) float64 {
|
|||||||
|
|
||||||
func (cfg *GameConfig) validateRefinery() error {
|
func (cfg *GameConfig) validateRefinery() error {
|
||||||
r := &cfg.Refinery
|
r := &cfg.Refinery
|
||||||
|
for _, item := range r.Items {
|
||||||
|
if item.UnlockGrade != "" && cfg.gradeIndex(item.UnlockGrade) < 0 {
|
||||||
|
return fmt.Errorf("refinery %q: unlockGrade %q is not a moon grade", item.Name, item.UnlockGrade)
|
||||||
|
}
|
||||||
|
}
|
||||||
if r.UnlockGrade != "" && cfg.gradeIndex(r.UnlockGrade) < 0 {
|
if r.UnlockGrade != "" && cfg.gradeIndex(r.UnlockGrade) < 0 {
|
||||||
return fmt.Errorf("refinery: unlockGrade %q is not a moon grade", r.UnlockGrade)
|
return fmt.Errorf("refinery: unlockGrade %q is not a moon grade", r.UnlockGrade)
|
||||||
}
|
}
|
||||||
@@ -350,6 +375,13 @@ func startRefine(ctx context.Context, db *pgxpool.Pool, userID, name string, qty
|
|||||||
return 0, gameErr("Unknown recipe")
|
return 0, gameErr("Unknown recipe")
|
||||||
}
|
}
|
||||||
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
||||||
|
var flags []string
|
||||||
|
if err := tx.QueryRow(ctx, "select onboarding from users where id = $1", userID).Scan(&flags); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !recipeOpen(item, flags) {
|
||||||
|
return gameErr("Buy a %s moon to refine %s", item.UnlockGrade, item.Name)
|
||||||
|
}
|
||||||
slots, level, err := lockRefinery(ctx, tx, userID)
|
slots, level, err := lockRefinery(ctx, tx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -709,6 +741,9 @@ func (s *State) refineryPage() *RefineryPage {
|
|||||||
}
|
}
|
||||||
for i := range c.Refinery.Items {
|
for i := range c.Refinery.Items {
|
||||||
item := &c.Refinery.Items[i]
|
item := &c.Refinery.Items[i]
|
||||||
|
if !recipeOpen(item, s.Onboarding) {
|
||||||
|
continue // (shows once a moon of its grade is bought)
|
||||||
|
}
|
||||||
card := RecipeCard{Item: item, Inputs: s.partRows(item.Inputs), MaxQty: maxBatch, Owned: s.have(item.Name)}
|
card := RecipeCard{Item: item, Inputs: s.partRows(item.Inputs), MaxQty: maxBatch, Owned: s.have(item.Name)}
|
||||||
for _, in := range card.Inputs {
|
for _, in := range card.Inputs {
|
||||||
card.InputValue += in.Need * c.unitPrice(in.Name)
|
card.InputValue += in.Need * c.unitPrice(in.Name)
|
||||||
|
|||||||
+2
-2
@@ -316,8 +316,8 @@ func sellTool(ctx context.Context, db *pgxpool.Pool, userID, toolID string) (str
|
|||||||
if _, err := lockPlayer(ctx, tx, userID); err != nil {
|
if _, err := lockPlayer(ctx, tx, userID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// A busy tool's idle copy is sold instead (see pickTool).
|
// (that tool: a busy one is refused below)
|
||||||
toolID, err := pickTool(ctx, tx, userID, toolID, false)
|
err := lockTool(ctx, tx, userID, toolID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- The moon grades each player has bought, as onboarding flags (grade:<G>), for recipes
|
||||||
|
-- that open with a grade (refinery.go recipeOpen). Idempotent: adds what is missing.
|
||||||
|
update users set onboarding = array(select distinct unnest(onboarding || g.flags))
|
||||||
|
from (
|
||||||
|
select user_id, array_agg(distinct 'grade:' || grade) as flags
|
||||||
|
from staking_source where grade <> '' group by user_id
|
||||||
|
) g
|
||||||
|
where users.id = g.user_id
|
||||||
|
and not (users.onboarding @> g.flags);
|
||||||
+3
-3
@@ -160,15 +160,15 @@ func (c *GameConfig) validateTracks(item StoreItem) error {
|
|||||||
|
|
||||||
// ------------------------------------------------------------------ rule
|
// ------------------------------------------------------------------ rule
|
||||||
|
|
||||||
// upgradeTool raises a tool's track of the kind (yield or cycle) a level (an idle copy of
|
// upgradeTool raises the tool's track of the kind (yield or cycle) a level (that tool, also
|
||||||
// it, if there is one: see pickTool) and returns the tool it upgraded.
|
// while it mines: from its next drill) and returns it.
|
||||||
func upgradeTool(ctx context.Context, db *pgxpool.Pool, userID, toolID, kind string) (string, error) {
|
func upgradeTool(ctx context.Context, db *pgxpool.Pool, userID, toolID, kind string) (string, error) {
|
||||||
if kind != "yield" && kind != "cycle" {
|
if kind != "yield" && kind != "cycle" {
|
||||||
return "", gameErr("Pick an upgrade track")
|
return "", gameErr("Pick an upgrade track")
|
||||||
}
|
}
|
||||||
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
||||||
var err error
|
var err error
|
||||||
if toolID, err = pickTool(ctx, tx, userID, toolID, false); err != nil {
|
if err = lockTool(ctx, tx, userID, toolID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var storeItemID int
|
var storeItemID int
|
||||||
|
|||||||
+4
-53
@@ -222,7 +222,6 @@ func (v View) resolve(s *State) View {
|
|||||||
if v.Section != "equipment" || s.tool(v.Drill) == nil || !s.Unlocked("equipment") {
|
if v.Section != "equipment" || s.tool(v.Drill) == nil || !s.Unlocked("equipment") {
|
||||||
v.Drill = ""
|
v.Drill = ""
|
||||||
}
|
}
|
||||||
v.Drill = s.idleCopy(v.Drill)
|
|
||||||
if v.Section != "advisors" || s.hire(v.Adv) == nil {
|
if v.Section != "advisors" || s.hire(v.Adv) == nil {
|
||||||
v.Adv = ""
|
v.Adv = ""
|
||||||
}
|
}
|
||||||
@@ -890,9 +889,12 @@ func (s *State) page(v View) *Page {
|
|||||||
sr := s.stakeRow(*st)
|
sr := s.stakeRow(*st)
|
||||||
row.Stake = &sr
|
row.Stake = &sr
|
||||||
}
|
}
|
||||||
|
if row.Stake == nil {
|
||||||
|
row.Idle = 1
|
||||||
|
}
|
||||||
|
// (every tool on its own card, copies too)
|
||||||
p.Equipment = append(p.Equipment, row)
|
p.Equipment = append(p.Equipment, row)
|
||||||
}
|
}
|
||||||
p.Equipment = groupCopies(s, p.Equipment)
|
|
||||||
for _, row := range p.Equipment {
|
for _, row := range p.Equipment {
|
||||||
if row.Selected {
|
if row.Selected {
|
||||||
p.Drill = s.drillPanel(row)
|
p.Drill = s.drillPanel(row)
|
||||||
@@ -954,21 +956,6 @@ func (s *State) copyKey(t Tool) string {
|
|||||||
return fmt.Sprint(t.StoreItemID, t.Levels())
|
return fmt.Sprint(t.StoreItemID, t.Levels())
|
||||||
}
|
}
|
||||||
|
|
||||||
// idleCopy is an idle copy of a busy tool (the tool itself if it's idle or has none), so
|
|
||||||
// selecting one of a group of copies never lands on a busy one while another is free.
|
|
||||||
func (s *State) idleCopy(id string) string {
|
|
||||||
t := s.tool(id)
|
|
||||||
if t == nil || s.StakeForTool(id) == nil {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
for _, c := range s.Tools {
|
|
||||||
if s.StakeForTool(c.ID) == nil && s.copyKey(c) == s.copyKey(*t) {
|
|
||||||
return c.ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
// followTool keeps the selected drill on the tool an action used: the action may have
|
// followTool keeps the selected drill on the tool an action used: the action may have
|
||||||
// used an idle copy (pickTool) of the one asked for.
|
// used an idle copy (pickTool) of the one asked for.
|
||||||
func followTool(selected, asked, used string) string {
|
func followTool(selected, asked, used string) string {
|
||||||
@@ -978,42 +965,6 @@ func followTool(selected, asked, used string) string {
|
|||||||
return selected
|
return selected
|
||||||
}
|
}
|
||||||
|
|
||||||
// groupCopies merges copies of a tool into one row (Count, Idle), shown by the selected one
|
|
||||||
// if it's among them, else an idle one, else the one finishing first.
|
|
||||||
func groupCopies(s *State, rows []ToolRow) []ToolRow {
|
|
||||||
var out []ToolRow
|
|
||||||
at := map[string]int{}
|
|
||||||
better := func(a, b ToolRow) bool { // a shows the group rather than b
|
|
||||||
switch {
|
|
||||||
case a.Selected != b.Selected:
|
|
||||||
return a.Selected
|
|
||||||
case (a.Stake == nil) != (b.Stake == nil):
|
|
||||||
return a.Stake == nil
|
|
||||||
case a.Stake != nil:
|
|
||||||
return a.Stake.Remaining < b.Stake.Remaining
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.Stake == nil {
|
|
||||||
row.Idle = 1
|
|
||||||
}
|
|
||||||
key := s.copyKey(row.Tool)
|
|
||||||
i, ok := at[key]
|
|
||||||
if !ok {
|
|
||||||
at[key] = len(out)
|
|
||||||
out = append(out, row)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
n, idle := out[i].Count+row.Count, out[i].Idle+row.Idle
|
|
||||||
if better(row, out[i]) {
|
|
||||||
out[i] = row
|
|
||||||
}
|
|
||||||
out[i].Count, out[i].Idle = n, idle
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// StakeURL opens the well a stake is on.
|
// StakeURL opens the well a stake is on.
|
||||||
func (p *Page) StakeURL(st *StakeRow) string {
|
func (p *Page) StakeURL(st *StakeRow) string {
|
||||||
return View{Section: "moons", Moon: st.MoonID, Well: st.WellID}.URL()
|
return View{Section: "moons", Moon: st.MoonID, Well: st.WellID}.URL()
|
||||||
|
|||||||
Reference in new issue
Block a user