Compare commits
5
Commits
7e18f99796
...
04dee47571
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04dee47571 | ||
|
|
d0716bf89a | ||
|
|
c278865a24 | ||
|
|
b76a189d22 | ||
|
|
069bf39ad2 |
No files matched your search
@@ -8,8 +8,8 @@ and stake them on wells. When a stake finishes (drill cycles are seconds:
|
|||||||
`completionTimeInSecs`, `duration_in_secs`) they claim resources (Sollux, Shadowstone,
|
`completionTimeInSecs`, `duration_in_secs`) they claim resources (Sollux, Shadowstone,
|
||||||
Azurium, Novafor, Nebulance), which they can sell for credits (₥) or refine into refined
|
Azurium, Novafor, Nebulance), which they can sell for credits (₥) or refine into refined
|
||||||
items (Refinery section: timed batches in slots; refined items sell for more and are parts
|
items (Refinery section: timed batches in slots; refined items sell for more and are parts
|
||||||
for higher tool upgrades). Drill attachments (bought in the Store, equipped on a drill from
|
for the higher upgrades). Each drill has two upgrade tracks of its own (Equipment: one raises
|
||||||
its panel in Equipment) give it a modifier that makes it faster or bigger; advisors
|
its claim per cycle, the other shortens its cycle, a level at a time, per tool); advisors
|
||||||
(Advisors section, one per seat) boost drill speed, yield, sell prices or the refinery for
|
(Advisors section, one per seat) boost drill speed, yield, sell prices or the refinery for
|
||||||
the whole account. There is also a leaderboard (Leaderboard section: top miners by credits,
|
the whole account. There is also a leaderboard (Leaderboard section: top miners by credits,
|
||||||
banned players left out).
|
banned players left out).
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
A native client for Moon Miners (rules and state live in the Go server: `../server`,
|
A native client for Moon Miners (rules and state live in the Go server: `../server`,
|
||||||
`../server/AGENTS.md`; the repo: `../AGENTS.md`). Same layout and HUD look as the web build
|
`../server/AGENTS.md`; the repo: `../AGENTS.md`). Same layout and HUD look as the web build
|
||||||
(header, moon list left, the section in the middle, aside with the moon viewport and the
|
(header, moon list left, the section in the middle, aside with the moon viewport and the
|
||||||
selected well), made game-native. Target 16:10 at 1280x800 (Steam Deck); bigger windows
|
selected well; on Moons the aside only shows, every action is on the well cards), made game-native. Target 16:10 at 1280x800 (Steam Deck); bigger windows
|
||||||
show the UI at native pixel size with more room, smaller ones rearrange. Mouse now,
|
show the UI at native pixel size with more room, smaller ones rearrange. Mouse now,
|
||||||
gamepad later. Details live in each file's header comment; this file has the rules and a
|
gamepad later. Details live in each file's header comment; this file has the rules and a
|
||||||
map.
|
map.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
-- Chip / badge (the web's .badge, tier chips, attachment tags): a 1 px outlined box with
|
-- Chip / badge (the web's .badge, level chips, advisor tags): a 1 px outlined box with
|
||||||
-- a short label, optionally filled. Returns the box node and its width.
|
-- a short label, optionally filled. Returns the box node and its width.
|
||||||
--
|
--
|
||||||
-- chip.new(parent, "T2/5", x, y, { color = "line_hi", text = "text_2", font = "mono" })
|
-- chip.new(parent, "Y2 C1", x, y, { color = "line_hi", text = "text_2", font = "mono" })
|
||||||
-- chip.new(parent, 3, x, y, { color = "ready", font = "mono", px = 12, min_w = 22 })
|
-- chip.new(parent, 3, x, y, { color = "ready", font = "mono", px = 12, min_w = 22 })
|
||||||
-- chip.new(parent, "Turbo core", x, y, { color = "violet", text = "violet_hi", upper = true })
|
-- chip.new(parent, "Turbo core", x, y, { color = "violet", text = "violet_hi", upper = true })
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -40,17 +40,17 @@ local GOLD_HI = C.ready_hi
|
|||||||
local LOSS = C.loss
|
local LOSS = C.loss
|
||||||
local FLASH = C.flash
|
local FLASH = C.flash
|
||||||
|
|
||||||
local BUY_KINDS = { ["buy-tool"] = true, upgrade = true, ["buy-moon"] = true, ["buy-attachment"] = true,
|
local BUY_KINDS = { ["buy-tool"] = true, upgrade = true, ["buy-moon"] = true,
|
||||||
equip = true, hire = true, ["upgrade-batch"] = true, ["buy-seat"] = true, ["seat-advisor"] = true, ["swap-advisors"] = true }
|
hire = true, ["upgrade-batch"] = true, ["buy-seat"] = true, ["seat-advisor"] = true, ["swap-advisors"] = true }
|
||||||
|
|
||||||
-- Sounds per action kind (fx.js); claim and sell sound as their numbers arrive.
|
-- Sounds per action kind (fx.js); claim and sell sound as their numbers arrive.
|
||||||
local SOUND = {
|
local SOUND = {
|
||||||
["buy-tool"] = "buy", ["buy-attachment"] = "buy", equip = "upgrade", unequip = "toggle",
|
["buy-tool"] = "buy",
|
||||||
hire = "confirm", ["seat-advisor"] = "upgrade", ["unseat-advisor"] = "toggle", ["swap-advisors"] = "toggle",
|
hire = "confirm", ["seat-advisor"] = "upgrade", ["unseat-advisor"] = "toggle", ["swap-advisors"] = "toggle",
|
||||||
["dismiss-advisor"] = "powerdown", ["buy-seat"] = "buy", ["buy-slot"] = "buy", upgrade = "upgrade", ["upgrade-batch"] = "upgrade",
|
["dismiss-advisor"] = "powerdown", ["buy-seat"] = "buy", ["buy-slot"] = "buy", upgrade = "upgrade", ["upgrade-batch"] = "upgrade",
|
||||||
["buy-moon"] = "moon", ["sell-moon"] = "shimmer", stake = "deploy", redrill = "deploy",
|
["buy-moon"] = "moon", ["sell-moon"] = "shimmer", stake = "deploy", redrill = "deploy",
|
||||||
refine = "refine", collect = "collect", error = "error",
|
refine = "refine", collect = "collect", error = "error",
|
||||||
-- (sell-tool and sell-attachment: the inline confirm's (cards.ask_button), then the cash as the coins land)
|
-- (sell-tool: the inline confirm's (cards.ask_button), then the cash as the coins land)
|
||||||
}
|
}
|
||||||
|
|
||||||
local function v3(x, y)
|
local function v3(x, y)
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ function M.copy(st)
|
|||||||
return copy(st, true)
|
return copy(st, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Copies of a tool: the same store item and track levels (tools, well.idle groups).
|
||||||
|
local function same_copy(a, b)
|
||||||
|
return a.storeItemId == b.storeItemId and (a.yieldLevel or 0) == (b.yieldLevel or 0)
|
||||||
|
and (a.cycleLevel or 0) == (b.cycleLevel or 0)
|
||||||
|
end
|
||||||
|
|
||||||
local function find(list, id, key)
|
local function find(list, id, key)
|
||||||
key = key or "id"
|
key = key or "id"
|
||||||
for i, x in ipairs(list or {}) do
|
for i, x in ipairs(list or {}) do
|
||||||
@@ -168,7 +174,7 @@ function M.tool(st, id)
|
|||||||
return find(st.tools, id)
|
return find(st.tools, id)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The tool to deploy for id: it, or an idle copy of it (same store item and tier).
|
-- The tool to deploy for id: it, or an idle copy of it (same store item and track levels).
|
||||||
function M.idle_copy(st, id)
|
function M.idle_copy(st, id)
|
||||||
local t = find(st.tools, id)
|
local t = find(st.tools, id)
|
||||||
if not t then
|
if not t then
|
||||||
@@ -178,7 +184,7 @@ function M.idle_copy(st, id)
|
|||||||
return t
|
return t
|
||||||
end
|
end
|
||||||
for _, x in ipairs(st.tools) do
|
for _, x in ipairs(st.tools) do
|
||||||
if not x.stakeId and x.storeItemId == t.storeItemId and x.tier == t.tier then
|
if not x.stakeId and same_copy(x, t) then
|
||||||
return x
|
return x
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -366,11 +372,11 @@ function M.add_stake(st, tool, moon_id, well, ctx, group)
|
|||||||
-- Off the selected well's idle list.
|
-- Off the selected well's idle list.
|
||||||
if st.well then
|
if st.well then
|
||||||
for i, g in ipairs(st.well.idle or {}) do
|
for i, g in ipairs(st.well.idle or {}) do
|
||||||
if g.storeItemId == tool.storeItemId and g.tier == tool.tier then
|
if same_copy(g, tool) then
|
||||||
g.count = g.count - 1
|
g.count = g.count - 1
|
||||||
if g.toolId == tool.id then
|
if g.toolId == tool.id then
|
||||||
for _, x in ipairs(st.tools) do
|
for _, x in ipairs(st.tools) do
|
||||||
if not x.stakeId and x.storeItemId == g.storeItemId and x.tier == g.tier then
|
if not x.stakeId and same_copy(x, g) then
|
||||||
g.toolId = x.id
|
g.toolId = x.id
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -414,7 +420,7 @@ function M.remove_stake(st, s)
|
|||||||
if st.well and st.well.idle then
|
if st.well and st.well.idle then
|
||||||
local g = nil
|
local g = nil
|
||||||
for _, x in ipairs(st.well.idle) do
|
for _, x in ipairs(st.well.idle) do
|
||||||
if x.storeItemId == tool.storeItemId and x.tier == tool.tier then
|
if same_copy(x, tool) then
|
||||||
g = x
|
g = x
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -424,9 +430,9 @@ function M.remove_stake(st, s)
|
|||||||
local free = M.supply(st, st.well.id)
|
local free = M.supply(st, st.well.id)
|
||||||
local y = tool.yield * yield_boost(st, st.well.resource)
|
local y = tool.yield * yield_boost(st, st.well.resource)
|
||||||
st.well.idle[#st.well.idle + 1] = {
|
st.well.idle[#st.well.idle + 1] = {
|
||||||
toolId = tool.id, storeItemId = tool.storeItemId, name = tool.name, tier = tool.tier, count = 1,
|
toolId = tool.id, storeItemId = tool.storeItemId, name = tool.name, yieldLevel = tool.yieldLevel, cycleLevel = tool.cycleLevel, count = 1,
|
||||||
cycleSecs = tool.cycleSecs, yield = y, reach = tool.reach, here = math.min(y, free), free = free,
|
cycleSecs = tool.cycleSecs, yield = y, reach = tool.reach, here = math.min(y, free), free = free,
|
||||||
short = free < y, attachments = tool.attachments or {},
|
short = free < y,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -667,7 +673,7 @@ M.register("stake", function(st, p, ctx)
|
|||||||
local group = nil
|
local group = nil
|
||||||
if st.well and st.well.id == w.id then
|
if st.well and st.well.id == w.id then
|
||||||
for _, g in ipairs(st.well.idle or {}) do
|
for _, g in ipairs(st.well.idle or {}) do
|
||||||
if g.storeItemId == tool.storeItemId and g.tier == tool.tier then
|
if same_copy(g, tool) then
|
||||||
group = g
|
group = g
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -705,7 +711,7 @@ M.register("redrill", function(st, p, ctx)
|
|||||||
local function group_of(t)
|
local function group_of(t)
|
||||||
if st.well and st.well.id == id then
|
if st.well and st.well.id == id then
|
||||||
for _, g in ipairs(st.well.idle or {}) do
|
for _, g in ipairs(st.well.idle or {}) do
|
||||||
if g.storeItemId == t.storeItemId and g.tier == t.tier then
|
if same_copy(g, t) then
|
||||||
return g
|
return g
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ M.palettes = {
|
|||||||
loss = { "#ff8a96" },
|
loss = { "#ff8a96" },
|
||||||
teal = { "#2dd4bf" },
|
teal = { "#2dd4bf" },
|
||||||
violet = { "#b69cff" },
|
violet = { "#b69cff" },
|
||||||
violet_hi = { "#dccfff" }, -- attachment and advisor effects
|
violet_hi = { "#dccfff" }, -- advisor effects
|
||||||
stat_up = { "#c4a8ff" }, -- a tool stat its next upgrade raises (not money)
|
stat_up = { "#c4a8ff" }, -- a tool stat its next upgrade raises (not money)
|
||||||
up = { "#3ee6a0" },
|
up = { "#3ee6a0" },
|
||||||
flash = { "#ffffff" }, -- peak of a gain flash or a scan line
|
flash = { "#ffffff" }, -- peak of a gain flash or a scan line
|
||||||
|
|||||||
@@ -35,17 +35,13 @@ local KIND = {
|
|||||||
redrill = { "cyan", "▼" },
|
redrill = { "cyan", "▼" },
|
||||||
upgrade = { "cyan", "▲" },
|
upgrade = { "cyan", "▲" },
|
||||||
["buy-tool"] = { "cyan", "+" },
|
["buy-tool"] = { "cyan", "+" },
|
||||||
["buy-attachment"] = { "cyan", "+" },
|
|
||||||
["buy-seat"] = { "cyan", "+" },
|
["buy-seat"] = { "cyan", "+" },
|
||||||
["buy-slot"] = { "cyan", "+" },
|
["buy-slot"] = { "cyan", "+" },
|
||||||
equip = { "cyan", "▲" },
|
|
||||||
unequip = { "muted", "▼" },
|
|
||||||
hire = { "violet", "●" },
|
hire = { "violet", "●" },
|
||||||
refine = { "violet", "◆" },
|
refine = { "violet", "◆" },
|
||||||
collect = { "teal", "◆" },
|
collect = { "teal", "◆" },
|
||||||
["sell-moon"] = { "ready", "₥" },
|
["sell-moon"] = { "ready", "₥" },
|
||||||
["sell-tool"] = { "ready", "₥" },
|
["sell-tool"] = { "ready", "₥" },
|
||||||
["sell-attachment"] = { "ready", "₥" },
|
|
||||||
["save-code"] = { "violet", "★" },
|
["save-code"] = { "violet", "★" },
|
||||||
restore = { "violet", "★" },
|
restore = { "violet", "★" },
|
||||||
error = { "danger", "!" },
|
error = { "danger", "!" },
|
||||||
|
|||||||
+16
-1
@@ -333,8 +333,13 @@ function M.dur(ms)
|
|||||||
return string.format("%d:%02d", m, sec)
|
return string.format("%d:%02d", m, sec)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A drill cycle like the web's secs: "4s", "2m", "2m 30s".
|
-- A drill cycle like the web's secs: "4s", "2.6s" (tenths), "2m", "2m 30s".
|
||||||
function M.cycle(secs)
|
function M.cycle(secs)
|
||||||
|
if secs < 60 and secs ~= math.floor(secs) then
|
||||||
|
local t = math.floor(secs * 10 + 0.5) / 10
|
||||||
|
return (t == math.floor(t) and string.format("%d", t) or string.format("%.1f", t)) .. "s"
|
||||||
|
end
|
||||||
|
secs = math.floor(secs + 0.5)
|
||||||
if secs < 60 then
|
if secs < 60 then
|
||||||
return secs .. "s"
|
return secs .. "s"
|
||||||
elseif secs % 60 == 0 then
|
elseif secs % 60 == 0 then
|
||||||
@@ -343,6 +348,16 @@ function M.cycle(secs)
|
|||||||
return string.format("%dm %ds", math.floor(secs / 60), secs % 60)
|
return string.format("%dm %ds", math.floor(secs / 60), secs % 60)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- A tool's track levels after its name, like the web's Levels.Tag: " Y2 C1", "" for a
|
||||||
|
-- base tool (both levels 0). t: anything with yieldLevel and cycleLevel.
|
||||||
|
function M.levels_tag(t)
|
||||||
|
local y, c = t and t.yieldLevel or 0, t and t.cycleLevel or 0
|
||||||
|
if y == 0 and c == 0 then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
return " Y" .. y .. " C" .. c
|
||||||
|
end
|
||||||
|
|
||||||
function M.plural(n, word)
|
function M.plural(n, word)
|
||||||
return n .. " " .. word .. (n == 1 and "" or "s")
|
return n .. " " .. word .. (n == 1 and "" or "s")
|
||||||
end
|
end
|
||||||
|
|||||||
+68
-282
@@ -1,26 +1,22 @@
|
|||||||
-- The aside (region, right column): the moon viewport (the selected moon in every
|
-- The aside (region, right column): the moon viewport (the selected moon in every
|
||||||
-- section, the shroud on New moon, a grid when there is none) and under it the selected
|
-- section, the shroud on New moon, a grid when there is none) and under it the selected
|
||||||
-- well's panel (supply, claim, redrill, deploy cards, drills), or what the section puts
|
-- well's facts (supply, what the drills leave, price, its drills and their times), the
|
||||||
-- there (a section module's aside / aside_below, see core/main.lua), always under the
|
-- moon's when no well is picked, or what the section puts there (a section module's
|
||||||
-- viewport.
|
-- 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).
|
||||||
|
|
||||||
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")
|
||||||
local tooltip = require("modules.tooltip")
|
local tooltip = require("modules.tooltip")
|
||||||
local row_c = require("components.row")
|
local row_c = require("components.row")
|
||||||
local button = require("components.button")
|
|
||||||
local chip = require("components.chip")
|
local chip = require("components.chip")
|
||||||
local anim = require("modules.anim")
|
|
||||||
local images = require("modules.images")
|
local images = require("modules.images")
|
||||||
local cards = require("screens.game.cards")
|
|
||||||
local ready = require("screens.game.core.ready")
|
|
||||||
local flex = require("modules.flex")
|
local flex = require("modules.flex")
|
||||||
local fg = require("modules.flex_gui")
|
local fg = require("modules.flex_gui")
|
||||||
|
|
||||||
@@ -94,45 +90,8 @@ local function reach_short(st, n)
|
|||||||
return pick and pick.short ~= "" and pick.short or (n .. " wells")
|
return pick and pick.short ~= "" and pick.short or (n .. " wells")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The long wording of a reach (catalog.reach label): "Mines 3 wells", "Strips the whole
|
|
||||||
-- moon"; "" for one.
|
|
||||||
local function reach_label(st, n)
|
|
||||||
local list = st.catalog and st.catalog.reach or {}
|
|
||||||
local pick = list[#list]
|
|
||||||
for _, r in ipairs(list) do
|
|
||||||
if r.reach == n then
|
|
||||||
pick = r
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return pick and pick.label or ""
|
|
||||||
end
|
|
||||||
|
|
||||||
-- An attachment's effect ("+20% speed", catalog), for its tag's tooltip.
|
|
||||||
local function effect_of(st, name)
|
|
||||||
for _, a in ipairs(st.catalog and st.catalog.attachments or {}) do
|
|
||||||
if a.name == name then
|
|
||||||
return a.effectText
|
|
||||||
end
|
|
||||||
end
|
|
||||||
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 width of a text in a style (a probe).
|
|
||||||
local function text_w(c, str, style)
|
|
||||||
local t = ui.text(c, str, style, 0, 0)
|
|
||||||
local w = ui.text_width(t)
|
|
||||||
gui.delete_node(t)
|
|
||||||
return w
|
|
||||||
end
|
|
||||||
|
|
||||||
-- A text link (H.text_link) as a box h tall.
|
|
||||||
local function link_box(g, c, H, str, style, h, v, id)
|
|
||||||
return made({ width = text_w(c, str, style), height = h, shrink = 0 }, function(p, x, y)
|
|
||||||
H.text_link(g, p, str, style, x, y + h / 2, v, id)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- A stat line (H.fact: cap label, value at the right; 20 tall).
|
-- A stat line (H.fact: cap label, value at the right; 20 tall).
|
||||||
local function fact(H, label, value, opts)
|
local function fact(H, label, value, opts)
|
||||||
return made({ height = 20 }, function(p, x, y, w)
|
return made({ height = 20 }, function(p, x, y, w)
|
||||||
@@ -145,115 +104,10 @@ 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" }
|
||||||
|
|
||||||
-- Deploy cards: one per idle tool group (well.idle), in a grid (app.css .deploy-cards:
|
|
||||||
-- 170 px columns, 8 apart). A card is the tool's picture on the dark media strip (3:2,
|
|
||||||
-- count and attachment pips over it), one line of name and tier, one of what a cycle gets
|
|
||||||
-- and Deploy, and a line kept for the short note: every card is the same height, so a
|
|
||||||
-- deploy never moves the cards after it (only a used-up group's card goes; the next one
|
|
||||||
-- slides into its spot). A tool that would get less than its yield here has an amber
|
|
||||||
-- edge; one that would get nothing (the well is spoken for) is also dimmed to .6 (.85
|
|
||||||
-- under the mouse) by a veil in the panel's color (.is-short / .is-spoken). The card is a
|
|
||||||
-- button built at its width; its content is a tree solved inside it.
|
|
||||||
local DEPLOY_MIN, DEPLOY_GAP = 170, 8
|
|
||||||
local function deploy_media_h(cw)
|
|
||||||
return math.floor(cw * 2 / 3)
|
|
||||||
end
|
|
||||||
local function deploy_card_h(cw)
|
|
||||||
return deploy_media_h(cw) + 1 + 7 + 16 + 3 + 16 + 3 + 14 + 8
|
|
||||||
end
|
|
||||||
local function deploy_card(g, c, well, grp, H, cw)
|
|
||||||
local hgt = deploy_card_h(cw)
|
|
||||||
local mh = deploy_media_h(cw)
|
|
||||||
local spoken = grp.short and grp.here <= 0
|
|
||||||
local edge = grp.short and { "ready", spoken and 0.35 or 0.45 } or nil
|
|
||||||
return made({ height = hgt }, function(p, x, y, inner)
|
|
||||||
-- blocked: the drills here take all it has left; the server refuses the stake.
|
|
||||||
local b, bn = ui.button(p, "", x, y, { w = inner, small = true, edge = edge, disabled = grp.blocked })
|
|
||||||
gui.set_size(b, vec(inner, hgt))
|
|
||||||
gui.set_size(ui.part(bn, "button", "edge"), vec(inner, hgt))
|
|
||||||
local tool = find(g.state.tools, grp.toolId)
|
|
||||||
local n_att = #(grp.attachments or {})
|
|
||||||
local gets_s = "+" .. ui.money(grp.here) .. " / " .. ui.cycle(grp.cycleSecs)
|
|
||||||
local reach = (grp.reach or 1) > 1 and fg.label(b, "· " .. reach_short(g.state, grp.reach), S12, 16, { fit = true, shrink = 1 }) or nil
|
|
||||||
if reach then
|
|
||||||
local label = reach_label(g.state, grp.reach)
|
|
||||||
if label ~= "" then
|
|
||||||
reach.done = function(n) tooltip.add(n, label) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local gets = fg.label(b, gets_s, M12, 16, { shrink = 0 })
|
|
||||||
gets.done = function(n)
|
|
||||||
tooltip.add(n, "What a cycle gets here after the drills already on it"
|
|
||||||
.. (grp.short and (" (its yield is " .. ui.money(grp.yield) .. ")") or ""))
|
|
||||||
end
|
|
||||||
local note = grp.short and fg.label(b, grp.here > 0 and ("Only " .. ui.money(grp.free) .. " left after current drills")
|
|
||||||
or "Well spoken for: nothing left", { font = "sans", px = 11, color = "ready" }, 14, { fit = true, shrink = 1 }) or space(0, 14)
|
|
||||||
local count = grp.count > 1 and ("×" .. grp.count) or nil
|
|
||||||
local count_w = count and text_w(b, count, { font = "mono", px = 12, color = "text" }) or 0
|
|
||||||
local tree = col({ width = inner, height = hgt }, {
|
|
||||||
-- The picture (a square, centered; grey when spoken for) on the media strip.
|
|
||||||
made({ height = mh, shrink = 0 }, function(q, ix, iy, iw)
|
|
||||||
ui.rect(q, ix + 1, iy + 1, iw - 2, mh - 1, "bg")
|
|
||||||
local img = ui.rect(q, ix + math.floor((iw - mh) / 2), iy + 1, mh - 1, mh - 1, "none")
|
|
||||||
if tool and tool.image then
|
|
||||||
images.set(img, tool.image)
|
|
||||||
if spoken then
|
|
||||||
-- Greyed (no grayscale filter here): a neutral grey over the picture.
|
|
||||||
ui.rect(q, ix + math.floor((iw - mh) / 2), iy + 1, mh - 1, mh - 1, "off", 0.6)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if n_att > 0 then
|
|
||||||
ui.rect(q, ix + 6, iy + 6, n_att * 13 + 4, 18, "panel", 0.82)
|
|
||||||
cards.pips(q, n_att, n_att, ix + 8, iy + 15)
|
|
||||||
end
|
|
||||||
if count then
|
|
||||||
ui.rect(q, ix + iw - 6 - count_w - 10, iy + 6, count_w + 10, 20, "panel", 0.82)
|
|
||||||
ui.text(q, count, { font = "mono", px = 12, color = "text" }, ix + iw - 6 - count_w - 5, iy + 16)
|
|
||||||
end
|
|
||||||
end),
|
|
||||||
made({ height = 1, shrink = 0 }, function(q, ix, iy, iw) ui.rect(q, ix + 1, iy, iw - 2, 1, "line") end),
|
|
||||||
col({ padding = { 7, 9, 8, 9 }, gap = 3, grow = 1 }, {
|
|
||||||
row({ height = 16, align = "center" }, {
|
|
||||||
fg.label(b, grp.name, { font = "sans_semi", px = 13, color = "text" }, 16, { fit = true, shrink = 1 }),
|
|
||||||
space(6), fg.label(b, "T" .. grp.tier, { font = "mono", px = 11, color = "cyan" }, 16, { shrink = 0 }),
|
|
||||||
}),
|
|
||||||
row({ height = 16, align = "center" }, {
|
|
||||||
gets, reach and space(5) or nil, reach, fg.fill(),
|
|
||||||
space(5), fg.label(b, "Deploy", { font = "sans_semi", px = 11, color = "cyan", tracking = 0.06, upper = true }, 16, { shrink = 0 }),
|
|
||||||
}),
|
|
||||||
note,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
fg.build(flex.solve(tree, 0, 0), b)
|
|
||||||
local title = "Deploy " .. grp.name .. " T" .. grp.tier
|
|
||||||
for _, a in ipairs(grp.attachments or {}) do
|
|
||||||
title = title .. " · " .. (type(a) == "table" and a.name or tostring(a))
|
|
||||||
end
|
|
||||||
tooltip.add(b, title)
|
|
||||||
if not grp.blocked then
|
|
||||||
H.action(g, b, bn, "stake", { tool = grp.toolId, target = well.id })
|
|
||||||
end
|
|
||||||
if spoken then
|
|
||||||
local veil = ui.rect(b, 0, 0, inner, hgt, "veil", 0.4)
|
|
||||||
if not grp.blocked then
|
|
||||||
g.druid:new_hover(b, nil, function(_, on)
|
|
||||||
gui.cancel_animations(veil, "color.w")
|
|
||||||
local a = on and 0.15 or 0.4
|
|
||||||
if anim.reduced or button.instant then
|
|
||||||
anim.set(veil, "color.w", a)
|
|
||||||
else
|
|
||||||
gui.animate(veil, "color.w", a, gui.EASING_OUTQUAD, 0.15)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
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, tier and the attachments' tags; 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)
|
||||||
local name, tier = moons.tool_name(st, d.toolId)
|
local name, tag = moons.tool_name(st, d.toolId)
|
||||||
local tool = find(st.tools, d.toolId)
|
local tool = find(st.tools, d.toolId)
|
||||||
return made({ height = 34 }, function(p, x, y, inner)
|
return made({ height = 34 }, function(p, x, y, inner)
|
||||||
local dr = ui.rect(p, x, y, inner, 34, "none")
|
local dr = ui.rect(p, x, y, inner, 34, "none")
|
||||||
@@ -268,29 +122,11 @@ local function drill_line(g, c, st, d, H, enters)
|
|||||||
end
|
end
|
||||||
local x0 = 36
|
local x0 = 36
|
||||||
local nt = ui.text(dr, name, CARD_NAME, x0, 8)
|
local nt = ui.text(dr, name, CARD_NAME, x0, 8)
|
||||||
local tt = ui.text(dr, "T" .. tier, M11, x0 + ui.text_width(nt) + 6, 8)
|
local head = { fg.node_label(nt, 16) }
|
||||||
-- Tags while they fit (and leave 50 for the time).
|
if tag ~= "" then
|
||||||
local head = { fg.node_label(nt, 16), space(6), fg.node_label(tt, 16), space(8) }
|
local tt = ui.text(dr, tag:sub(2), M11, x0 + ui.text_width(nt) + 6, 8)
|
||||||
local tx = x0 + ui.text_width(nt) + 6 + ui.text_width(tt) + 8
|
head[#head + 1] = space(6)
|
||||||
for _, a in ipairs(tool and tool.attachments or {}) do
|
head[#head + 1] = fg.node_label(tt, 16)
|
||||||
if tx + 60 > inner - 50 then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
local probe = ui.rect(dr, 0, 0, 1, 1, "none")
|
|
||||||
local aw = cards.tag(probe, a, 0, 0)
|
|
||||||
gui.delete_node(probe)
|
|
||||||
if tx + aw > inner - 50 then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
head[#head + 1] = made({ width = aw, height = 16, shrink = 0 }, function(q, ax, ay)
|
|
||||||
cards.tag(q, a, ax, ay + 8)
|
|
||||||
local fx_text = effect_of(st, a)
|
|
||||||
if fx_text then
|
|
||||||
tooltip.add(ui.rect(q, ax, ay, aw, 16, "none"), fx_text)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
head[#head + 1] = space(4)
|
|
||||||
tx = tx + aw + 4
|
|
||||||
end
|
end
|
||||||
-- The amount it gets in ready when it comes up short (engine markup runs).
|
-- The amount it gets in ready when it comes up short (engine markup runs).
|
||||||
local reach = (d.reach or 1) > 1 and (" · " .. ((d.reachText or "") ~= "" and d.reachText or reach_short(st, d.reach))) or ""
|
local reach = (d.reach or 1) > 1 and (" · " .. ((d.reachText or "") ~= "" and d.reachText or reach_short(st, d.reach))) or ""
|
||||||
@@ -335,11 +171,6 @@ local function drill_line(g, c, st, d, H, enters)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A row of claim buttons (Claim / its countdown, and Claim and redeploy beside it).
|
|
||||||
local function claim_row(kids)
|
|
||||||
return row({ height = S.button_h, gap = 8 }, kids)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- The selected well under the viewport (one tree; it scrolls); returns its bottom.
|
-- The selected well under the viewport (one tree; it scrolls); returns its bottom.
|
||||||
local function well_panel(g, root, pad, top, inner, H)
|
local function well_panel(g, root, pad, top, inner, H)
|
||||||
local st = g.state
|
local st = g.state
|
||||||
@@ -376,8 +207,7 @@ local function well_panel(g, root, pad, top, inner, H)
|
|||||||
-- The well note in the server's words (the web's): the lead (over-drilled, fully
|
-- The well note in the server's words (the web's): the lead (over-drilled, fully
|
||||||
-- committed) in the warning color, then what the drills take of it, amber once
|
-- committed) in the warning color, then what the drills take of it, amber once
|
||||||
-- they want it all; one paragraph (engine markup runs). Its two lines are kept
|
-- they want it all; one paragraph (engine markup runs). Its two lines are kept
|
||||||
-- when there is no note, and "After current drills" is always there: the first
|
-- when there is no note, so a deploy doesn't make the facts jump.
|
||||||
-- deploy must not push the deploy cards down (buttons never move under the pointer).
|
|
||||||
local note = nil
|
local note = nil
|
||||||
if (wrow.noteText or "") ~= "" then
|
if (wrow.noteText or "") ~= "" then
|
||||||
local runs = {}
|
local runs = {}
|
||||||
@@ -427,50 +257,6 @@ local function well_panel(g, root, pad, top, inner, H)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The web's order: deploy, drills, then the claims and Redrill.
|
|
||||||
if not dry then
|
|
||||||
add(fg.label(c, "Deploy a tool", "h3", 12))
|
|
||||||
add(space(0, 6))
|
|
||||||
if #w.idle > 0 then
|
|
||||||
local cards_ = {}
|
|
||||||
local _, cw = flex.columns(inner, DEPLOY_MIN, DEPLOY_GAP)
|
|
||||||
for _, grp in ipairs(w.idle) do
|
|
||||||
cards_[#cards_ + 1] = deploy_card(g, c, w, grp, H, cw)
|
|
||||||
end
|
|
||||||
add(flex.grid(inner, DEPLOY_MIN, DEPLOY_GAP, cards_))
|
|
||||||
add(space(0, 6))
|
|
||||||
local better = w.better
|
|
||||||
if better then
|
|
||||||
-- "Better: <resource> well on <moon>, X free": the well is a link.
|
|
||||||
local where = better.moonId ~= moon.id and find(st.moons, better.moonId)
|
|
||||||
local rest = (where and (" on " .. where.name) or "") .. ", " .. ui.money(better.free) .. " free"
|
|
||||||
local name, tier = moons.tool_name(st, better.toolId)
|
|
||||||
add(row({ height = 40, align = "start" }, {
|
|
||||||
row({ height = S.button_sm_h, grow = 1, shrink = 1, align = "center", padding_right = 8 }, {
|
|
||||||
fg.label(c, "Better:", "small", 16), space(4),
|
|
||||||
made({ width = 8, height = 8, shrink = 0 }, function(p, x, y) ui.rect(p, x, y, 8, 8, better.resource) end),
|
|
||||||
space(4),
|
|
||||||
link_box(g, c, H, better.resource .. " well", "small", 16,
|
|
||||||
{ section = "moons", moon = better.moonId, well = better.wellId }, "better-well"),
|
|
||||||
fg.label(c, rest, "small", 16, { fit = not ui.needs_rich(rest), shrink = 1 }),
|
|
||||||
}),
|
|
||||||
fg.button(c, "Deploy there", { small = true, primary = true }, function(b, bn)
|
|
||||||
H.action(g, b, bn, "stake", { tool = better.toolId, target = better.wellId, s = "moons", moon = better.moonId, well = better.wellId })
|
|
||||||
tooltip.add(b, "Deploy " .. name .. " T" .. tier .. " on that well")
|
|
||||||
end),
|
|
||||||
}))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
add(row({ height = 16, align = "center" }, {
|
|
||||||
fg.label(c, "No idle tools.", "small", 16),
|
|
||||||
st.unlocked.store and space(5) or nil,
|
|
||||||
st.unlocked.store and link_box(g, c, H, "Buy one in the store", { font = "sans", px = 13 }, 16, { section = "store", moon = g.view.moon }) or nil,
|
|
||||||
st.unlocked.store and fg.label(c, ".", "small", 16) or nil,
|
|
||||||
}))
|
|
||||||
add(space(0, 6))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Drills on this well, its own and multi-well ones, oldest first (the web's Queue).
|
-- Drills on this well, its own and multi-well ones, oldest first (the web's Queue).
|
||||||
local drills = {}
|
local drills = {}
|
||||||
for _, d in ipairs(wrow.drills) do
|
for _, d in ipairs(wrow.drills) do
|
||||||
@@ -499,62 +285,22 @@ local function well_panel(g, root, pad, top, inner, H)
|
|||||||
add(col({ gap = 2 }, lines))
|
add(col({ gap = 2 }, lines))
|
||||||
add(space(0, #lines > 0 and 10 or 8))
|
add(space(0, #lines > 0 and 10 or 8))
|
||||||
|
|
||||||
-- Claim (ready), Claim countdown (mining), Claim and redeploy; the multi-well claim;
|
-- Idle drills that last mined it (Redrill on its card), and a better well for the
|
||||||
-- Redrill. Amounts move on as more drills finish on our clock (core/ready).
|
-- idle drills (the server's pick), as text.
|
||||||
local claims = {}
|
if (wrow.redrill or 0) > 0 then
|
||||||
local amount = ready.amount(g, wrow, wrow.supply)
|
add(fact(H, "Idle, last mined here", tostring(wrow.redrill)))
|
||||||
if wrow.ready > 0 then
|
add(space(0, 6))
|
||||||
claims[#claims + 1] = claim_row({
|
|
||||||
fg.button(c, "Claim " .. ui.money(amount), { primary = true }, function(b, bn)
|
|
||||||
H.action(g, b, bn, "claim", { ["claim-well"] = w.id })
|
|
||||||
end),
|
|
||||||
(not dry and st.unlocked.redeploy) and fg.button(c, "Claim and redeploy", {}, function(b, bn)
|
|
||||||
H.action(g, b, bn, "claim", { ["claim-well"] = w.id, redeploy = "1" })
|
|
||||||
end) or nil,
|
|
||||||
})
|
|
||||||
elseif wrow.nextEnd then
|
|
||||||
claims[#claims + 1] = made({ height = S.button_h }, function(p, x, y)
|
|
||||||
H.claim_wait(g, p, x, y, wrow.nextEnd, { key = "wait:well", params = { ["claim-well"] = w.id }, moon = moon.id,
|
|
||||||
done = amount > 0 and ("Claim " .. ui.money(amount)) or nil,
|
|
||||||
also = not dry and st.unlocked.redeploy
|
|
||||||
and { label = "Claim and redeploy", params = { ["claim-well"] = w.id, redeploy = "1" } } or nil })
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
if #wide_here > 0 then
|
local better = w.better
|
||||||
local wamount = ready.amount(g, wide, wide.cap)
|
if better and not dry then
|
||||||
if wide.ready > 0 then
|
local where = better.moonId ~= moon.id and find(st.moons, better.moonId)
|
||||||
claims[#claims + 1] = claim_row({
|
local name, tag = moons.tool_name(st, better.toolId)
|
||||||
fg.button(c, "Claim " .. ui.money(wamount), { primary = true }, function(b, bn)
|
add(fg.text_para(c, "Better for " .. name .. tag .. ": the " .. better.resource .. " well"
|
||||||
H.action(g, b, bn, "claim", { ["claim-wide"] = moon.id })
|
.. (where and (" on " .. where.name) or "") .. ", " .. ui.money(better.free) .. " free.", "small", { max_width = math.min(inner, S.text_max) }))
|
||||||
end),
|
add(space(0, 6))
|
||||||
st.unlocked.redeploy and fg.button(c, "Claim and redeploy", {}, function(b, bn)
|
|
||||||
H.action(g, b, bn, "claim", { ["claim-wide"] = moon.id, redeploy = "1" })
|
|
||||||
end) or nil,
|
|
||||||
})
|
|
||||||
elseif wide.nextEnd then
|
|
||||||
claims[#claims + 1] = made({ height = S.button_h }, function(p, x, y)
|
|
||||||
H.claim_wait(g, p, x, y, wide.nextEnd, { params = { ["claim-wide"] = moon.id }, moon = moon.id,
|
|
||||||
done = wamount > 0 and ("Claim " .. ui.money(wamount)) or nil })
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if (wrow.redrill or 0) > 0 and not dry and st.unlocked.redrill then
|
|
||||||
claims[#claims + 1] = claim_row({
|
|
||||||
fg.button(c, "Redrill" .. (wrow.redrill > 1 and (" " .. wrow.redrill) or ""), { variant = "redrill" }, function(b, bn)
|
|
||||||
H.action(g, b, bn, "redrill", { ["redrill-well"] = w.id })
|
|
||||||
end),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
add(col({ gap = 10, padding_bottom = #claims > 0 and 10 or 0 }, claims))
|
|
||||||
|
|
||||||
-- Selling the moon (once the store is open), at the foot of the moon's pane.
|
|
||||||
if st.unlocked.store and moon.sale then
|
|
||||||
add(space(0, 14))
|
|
||||||
add(made({ height = 1 }, function(p, x, y, iw) ui.rect(p, x, y, iw, 1, "line") end))
|
|
||||||
add(space(0, 12))
|
|
||||||
add(moon_sale.box(g, H, c, moon, inner))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- (no buttons here: every action on the well is on its card in the main panel)
|
||||||
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
|
||||||
@@ -590,6 +336,45 @@ function M.row_viewport(w)
|
|||||||
return math.floor(math.max(240, math.min(560, 380 + (inner - 16 - 660) / 2)))
|
return math.floor(math.max(240, math.min(560, 380 + (inner - 16 - 660) / 2)))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The selected moon's facts under the viewport when no well is picked (read only).
|
||||||
|
local function moon_status(g, c, x, y, inner, H)
|
||||||
|
local st = g.state
|
||||||
|
local moon = st.moon
|
||||||
|
local supply, dry, mining, done = 0, 0, 0, 0
|
||||||
|
for _, w in ipairs(moon.wells or {}) do
|
||||||
|
supply = supply + w.supply
|
||||||
|
dry = dry + (w.supply <= 0 and 1 or 0)
|
||||||
|
end
|
||||||
|
local now = H.now(g)
|
||||||
|
for _, s in ipairs(st.stakes or {}) do
|
||||||
|
if s.moonId == moon.id then
|
||||||
|
if s.ready or (s["end"] or math.huge) <= now then
|
||||||
|
done = done + 1
|
||||||
|
else
|
||||||
|
mining = mining + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local idle = 0
|
||||||
|
for _, t in ipairs(st.tools or {}) do
|
||||||
|
idle = idle + (t.stakeId and 0 or 1)
|
||||||
|
end
|
||||||
|
local kids = {
|
||||||
|
fg.label(c, moon.name, "h2", 18), space(0, 10),
|
||||||
|
fg.text_para(c, "Click a well's card for its details.", "small", { max_width = math.min(inner, S.text_max) }),
|
||||||
|
space(0, 10),
|
||||||
|
fact(H, "Wells", tostring(#(moon.wells or {})) .. (dry > 0 and (" · " .. dry .. " tapped out") or "")),
|
||||||
|
fact(H, "Supply left", ui.money(supply)),
|
||||||
|
fact(H, "Drills mining", tostring(mining)),
|
||||||
|
fact(H, "Ready to claim", tostring(done), { color = done > 0 and "ready" or "text" }),
|
||||||
|
fact(H, "Idle drills", tostring(idle)),
|
||||||
|
}
|
||||||
|
if st.unlocked.store and moon.sale then
|
||||||
|
kids[#kids + 1] = fact(H, "Sells for", moon.sale.total, { credits = true })
|
||||||
|
end
|
||||||
|
fg.build(flex.solve(col({ width = inner }, kids), x, y), c)
|
||||||
|
end
|
||||||
|
|
||||||
-- A heading (h2) and a line under the viewport.
|
-- A heading (h2) and a line under the viewport.
|
||||||
local function caption(c, x, y, inner, title, text)
|
local function caption(c, x, y, inner, title, text)
|
||||||
fg.build(flex.solve(col({ width = inner }, {
|
fg.build(flex.solve(col({ width = inner }, {
|
||||||
@@ -645,13 +430,14 @@ function M.build(g, parent, x, y, w, h, H, mod, row)
|
|||||||
gui.delete_node(box)
|
gui.delete_node(box)
|
||||||
end
|
end
|
||||||
if moon and st.well then
|
if moon and st.well then
|
||||||
-- The well panel scrolls under the viewport (many deploy cards or drills).
|
-- The well panel scrolls under the viewport (many drills).
|
||||||
local top = cy - 8
|
local top = cy - 8
|
||||||
local area = scroll.area(g, root, 0, top, w, h - top - 4, "well:" .. st.well.id)
|
local area = scroll.area(g, root, 0, top, w, h - top - 4, "well:" .. st.well.id)
|
||||||
area:finish(well_panel(g, area.content, pad, 8, inner, H) + pad)
|
area:finish(well_panel(g, area.content, pad, 8, inner, H) + pad)
|
||||||
|
elseif moon and moon.finished then
|
||||||
|
caption(root, pad, cy, inner, moon.name, "Mined out. Nothing left to dig.")
|
||||||
elseif moon then
|
elseif moon then
|
||||||
caption(root, pad, cy, inner, moon.name, moon.finished and "Mined out. Nothing left to dig."
|
moon_status(g, root, pad, cy, inner, H)
|
||||||
or "Pick a well in the table to deploy tools and claim resources.")
|
|
||||||
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
|
||||||
|
|||||||
@@ -48,14 +48,13 @@ function M.pct(m)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local EFFECT_WORDS = {
|
local EFFECT_WORDS = {
|
||||||
attachment = { { "speed", "speed" }, { "yield", "yield" } },
|
|
||||||
advisor = { { "speed", "drill speed" }, { "yield", "yield" }, { "sell", "sell prices" }, { "refine", "refinery speed" } },
|
advisor = { { "speed", "drill speed" }, { "yield", "yield" }, { "sell", "sell prices" }, { "refine", "refinery speed" } },
|
||||||
}
|
}
|
||||||
|
|
||||||
-- "+20% speed", "+5% drill speed, +5% yield" (kind: "attachment" or "advisor").
|
-- "+5% drill speed, +5% yield" (kind: "advisor").
|
||||||
function M.effect(effect, kind)
|
function M.effect(effect, kind)
|
||||||
local parts = {}
|
local parts = {}
|
||||||
for _, e in ipairs(EFFECT_WORDS[kind or "attachment"]) do
|
for _, e in ipairs(EFFECT_WORDS[kind or "advisor"]) do
|
||||||
local m = effect and effect[e[1]] or 1
|
local m = effect and effect[e[1]] or 1
|
||||||
if m > 1 then
|
if m > 1 then
|
||||||
parts[#parts + 1] = M.pct(m) .. " " .. e[2]
|
parts[#parts + 1] = M.pct(m) .. " " .. e[2]
|
||||||
@@ -187,29 +186,12 @@ function M.stat_up(parent, label, from, to, x, y)
|
|||||||
return w
|
return w
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A violet tag (attachment names); returns its width.
|
-- A violet tag (advisor labels); returns its width and the box.
|
||||||
function M.tag(parent, text, x, y, color)
|
function M.tag(parent, text, x, y, color)
|
||||||
local box, w = chip.new(parent, text, x, y, { color = color or "violet", alpha = 0.5, text = color or "violet_hi", upper = true })
|
local box, w = chip.new(parent, text, x, y, { color = color or "violet", alpha = 0.5, text = color or "violet_hi", upper = true })
|
||||||
return w, box
|
return w, box
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Attachment slot pips: filled diamonds for used slots. Returns the width.
|
|
||||||
function M.pips(parent, used, slots, x, y)
|
|
||||||
for i = 1, slots do
|
|
||||||
local p = ui.rect(parent, x + (i - 1) * 13 + 5, y, 8, 8, "violet", i <= used and 1 or 0)
|
|
||||||
gui.set_pivot(p, gui.PIVOT_CENTER)
|
|
||||||
gui.set_position(p, vec(x + (i - 1) * 13 + 5, -y))
|
|
||||||
gui.set_rotation(p, vmath.vector3(0, 0, 45))
|
|
||||||
if i > used then
|
|
||||||
local o = ui.outline(parent, 0, 0, 8, 8, "violet")
|
|
||||||
gui.set_pivot(o, gui.PIVOT_CENTER)
|
|
||||||
gui.set_position(o, vec(x + (i - 1) * 13 + 5, -y))
|
|
||||||
gui.set_rotation(o, vmath.vector3(0, 0, 45))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return slots * 13
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Refined parts an upgrade needs: "● 3/5 Dusk Alloy" chips, red when short. Returns the
|
-- Refined parts an upgrade needs: "● 3/5 Dusk Alloy" chips, red when short. Returns the
|
||||||
-- height used.
|
-- height used.
|
||||||
function M.parts(st, parent, parts, x, y, w)
|
function M.parts(st, parent, parts, x, y, w)
|
||||||
@@ -282,7 +264,7 @@ end
|
|||||||
|
|
||||||
---------------------------------------------------------------- inline confirm
|
---------------------------------------------------------------- inline confirm
|
||||||
|
|
||||||
-- Sells ask in place, not in a dialog (sell-tool, sell-attachment, sell-moon): the first
|
-- Sells ask in place, not in a dialog (sell-tool, sell-moon): the first
|
||||||
-- click on the button (M.ask_button) puts it in its asking state, which its section builds
|
-- click on the button (M.ask_button) puts it in its asking state, which its section builds
|
||||||
-- instead of the button in the same footprint (M.asking, M.ask_buttons, M.ask_text): the
|
-- instead of the button in the same footprint (M.asking, M.ask_buttons, M.ask_text): the
|
||||||
-- button's spot becomes Cancel, Confirm (the server's OK words, "Sell · ₥750") sits just
|
-- button's spot becomes Cancel, Confirm (the server's OK words, "Sell · ₥750") sits just
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ local function build_main(g, parent, x, y, w, h)
|
|||||||
local st = g.state
|
local st = g.state
|
||||||
local section = g.view.section
|
local section = g.view.section
|
||||||
local common = local_sig(g, MAIN_READY[section] or {})
|
local common = local_sig(g, MAIN_READY[section] or {})
|
||||||
local sig = blocks.sig(common, reveal_sig(g), x, y, w, h, g.side_w, g.view, g.advisor_pick, g.refine_qty, g.sale_open, g.nm_grade,
|
local sig = blocks.sig(common, reveal_sig(g), x, y, w, h, g.side_w, g.view, g.advisor_pick, g.refine_qty, g.sale_open, g.nm_grade, g.deploy_pick or false,
|
||||||
pick_keys(st, ALWAYS, SECTION_KEYS[section] or {}))
|
pick_keys(st, ALWAYS, SECTION_KEYS[section] or {}))
|
||||||
blocks.keep(g, "main", sig, parent, function(holder)
|
blocks.keep(g, "main", sig, parent, function(holder)
|
||||||
for _, m in pairs(M.SECTIONS) do
|
for _, m in pairs(M.SECTIONS) do
|
||||||
@@ -446,10 +446,11 @@ function M.build(g)
|
|||||||
moon_view.move(g.viewport_node) -- (where the page's offset put it)
|
moon_view.move(g.viewport_node) -- (where the page's offset put it)
|
||||||
g.viewport_rect = g.fx:rect_of(g.viewport_node)
|
g.viewport_rect = g.fx:rect_of(g.viewport_node)
|
||||||
end
|
end
|
||||||
-- Picking a well, a drill or an advisor brings the aside into view.
|
-- Picking a drill or an advisor brings the aside into view (not a well: its card,
|
||||||
|
-- where its actions are, stays in view; the aside only shows its facts).
|
||||||
local v = g.view
|
local v = g.view
|
||||||
local key = table.concat({ v.moon or "", v.well or "", v.drill or "", tostring(g.advisor_pick) }, "|")
|
local key = table.concat({ v.moon or "", v.well or "", v.drill or "", tostring(g.advisor_pick) }, "|")
|
||||||
local want = (v.well or "") ~= "" or (v.drill or "") ~= "" or v.section == "advisors"
|
local want = (v.drill or "") ~= "" or v.section == "advisors"
|
||||||
if s and want and key ~= g.page_key then
|
if s and want and key ~= g.page_key then
|
||||||
s:scroll_to(vmath.vector3(0, -(y0 + body_h + gap - pad), 0))
|
s:scroll_to(vmath.vector3(0, -(y0 + body_h + gap - pad), 0))
|
||||||
end
|
end
|
||||||
|
|||||||
+164
-234
@@ -1,11 +1,12 @@
|
|||||||
-- The Equipment section (state.equipment): a card per tool (copies of the same type, tier
|
-- The Equipment section (state.equipment): a card per tool (copies of the same type and
|
||||||
-- and attachments share one) with its image, tier, status, stats and what the next
|
-- track levels share one) with its image, levels chip, status, stats (yield, cycle) and
|
||||||
-- upgrade gives (stat_up violet, apart from money), attachment pips and tags, and the
|
-- its two upgrade tracks side by side (tracks.go): each track's name and level, its value
|
||||||
-- upgrade button with its parts. A card opens the drill panel (view drill=<tool id>) in
|
-- now ▸ after the next level (stat_up violet, apart from money), the Upgrade button (or
|
||||||
-- the aside: stats, upgrade, tier ladder, slots (unequip), spare attachments (equip, with
|
-- Max level in its place) and a need line kept whether it shows, so the buttons never
|
||||||
-- the stats they'd give; sell), idle copies and their best well, and selling the drill.
|
-- move (clicker rule). A card opens the drill panel (view drill=<tool id>) in the aside:
|
||||||
-- Before the Store is unlocked (onboarding) cards and panel leave out the Store's parts
|
-- stats, the two tracks, each track's ladder, idle copies and their best well, and
|
||||||
-- (attachments, upgrades, selling): the panel is stats, tiers and idle copies.
|
-- selling the drill. Before the Store is unlocked (onboarding) cards and panel leave out
|
||||||
|
-- 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")
|
||||||
@@ -32,36 +33,22 @@ local function find(list, key, value)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function catalog_attachment(st, name)
|
|
||||||
return find(st.catalog and st.catalog.attachments, "name", name) or { effect = {} }
|
|
||||||
end
|
|
||||||
|
|
||||||
local function drill_view(self, id)
|
local function drill_view(self, id)
|
||||||
return { section = "equipment", drill = id, moon = self.view.moon, well = self.view.well }
|
return { section = "equipment", drill = id, moon = self.view.moon, well = self.view.well }
|
||||||
end
|
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
|
||||||
|
|
||||||
-- Stats with the next upgrade's changes: Tier, Yield, Reach (when it grows), Cycle,
|
-- Stats: Yield and Cycle (a stake's now), wrapping (rows 20 tall, 16 apart). A tree piece.
|
||||||
-- wrapping (rows 20 tall, 16 apart). A tree piece.
|
|
||||||
local function stats_box(st, parent, t)
|
local function stats_box(st, parent, t)
|
||||||
local u = t.preview
|
local items = { { "Yield", ui.money(t.yield) }, { "Cycle", ui.cycle(t.cycleSecs) } }
|
||||||
local items = {}
|
|
||||||
if u then
|
|
||||||
items[#items + 1] = { "Tier", "T" .. t.tier, "T" .. u.tier }
|
|
||||||
end
|
|
||||||
items[#items + 1] = { "Yield", ui.money(t.yield), u and u.yieldTo > u.yieldFrom + 1e-9 and ui.money(u.yieldTo) or nil }
|
|
||||||
if u and u.reachTo > u.reachFrom then
|
|
||||||
items[#items + 1] = { "Reach", tostring(u.reachFrom), cards.wells(st, u.reachTo) }
|
|
||||||
end
|
|
||||||
items[#items + 1] = { "Cycle", ui.cycle(t.cycleSecs), u and u.secsTo ~= u.secsFrom and ui.cycle(u.secsTo) or nil }
|
|
||||||
local kids = {}
|
local kids = {}
|
||||||
for _, it in ipairs(items) do
|
for _, it in ipairs(items) do
|
||||||
local probe = ui.rect(parent, 0, 0, 1, 1, "none")
|
local probe = ui.rect(parent, 0, 0, 1, 1, "none")
|
||||||
local iw = cards.stat_up(probe, it[1], it[2], it[3], 0, 0)
|
local iw = cards.stat_up(probe, it[1], it[2], nil, 0, 0)
|
||||||
gui.delete_node(probe)
|
gui.delete_node(probe)
|
||||||
kids[#kids + 1] = made({ width = iw, height = 20, shrink = 0 }, function(p, x, y)
|
kids[#kids + 1] = made({ width = iw, height = 20, shrink = 0 }, function(p, x, y)
|
||||||
cards.stat_up(p, it[1], it[2], it[3], x, y + 8)
|
cards.stat_up(p, it[1], it[2], nil, x, y + 8)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
return row({ wrap = true, column_gap = 16 }, kids)
|
return row({ wrap = true, column_gap = 16 }, kids)
|
||||||
@@ -75,56 +62,95 @@ local function stats(st, parent, t, x, y, w)
|
|||||||
return tree.h
|
return tree.h
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The upgrade button (or "Max tier"), what's short and the parts. A tree piece w wide.
|
-- A track's value: "+5" (yield per cycle) or "2.6s" (cycle).
|
||||||
local function upgrade_box(self, H, st, parent, t, w, small)
|
local function track_value(tr, v)
|
||||||
local n = t.next
|
return tr.kind == "cycle" and ui.cycle(v) or ("+" .. ui.money(v))
|
||||||
|
end
|
||||||
|
|
||||||
|
local TRACK_NAME = { font = "sans_semi", px = 12, color = "text_2", tracking = 0.04 }
|
||||||
|
local M12 = { font = "mono", px = 12, color = "muted" }
|
||||||
|
|
||||||
|
-- One upgrade track, a column w wide: name and level (16), value ▸ next (18), the button
|
||||||
|
-- (or Max level, the same size), the need line (18, kept: "Need ₥X more" or blank) and
|
||||||
|
-- the parts it uses. Every box has these rows, so both buttons of a card sit at the same
|
||||||
|
-- height and never move. A tree piece.
|
||||||
|
local function track_box(self, H, st, parent, t, tr, w)
|
||||||
|
local n = tr.next
|
||||||
|
local val = made({ height = 18, shrink = 0 }, function(p, x, y)
|
||||||
|
local now = ui.text(p, track_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, track_value(tr, n.value), { font = "mono_semi", px = 13, color = "stat_up" }, ax, y + 9)
|
||||||
|
tooltip.add(nx, "After the upgrade")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
local kids = {
|
||||||
|
row({ height = 16, align = "center" }, {
|
||||||
|
fg.label(parent, tr.name, TRACK_NAME, 16, { fit = true, shrink = 1, shrink_text = 0.85 }),
|
||||||
|
fg.fill(), space(6),
|
||||||
|
fg.label(parent, tr.level .. "/" .. tr.max, M12, 16, { shrink = 0 }),
|
||||||
|
}),
|
||||||
|
val,
|
||||||
|
space(0, 2),
|
||||||
|
}
|
||||||
|
-- The parts line keeps the room of the most any level of the track needs (measured at
|
||||||
|
-- this width), so the box (and the card) stays the same height from level to level.
|
||||||
|
local parts_h = 0
|
||||||
|
local probe = ui.rect(parent, 0, 0, 1, 1, "none")
|
||||||
|
for _, r in ipairs(tr.ladder or {}) do
|
||||||
|
if #(r.parts or {}) > 0 then
|
||||||
|
parts_h = math.max(parts_h, cards.parts(st, probe, r.parts, 0, 0, w))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
gui.delete_node(probe)
|
||||||
if not n then
|
if not n then
|
||||||
return col({ width = w }, { fg.label(parent, "Max tier", "small", 20), space(0, 2) })
|
kids[#kids + 1] = fg.button(parent, "Max level", { small = true, disabled = true }, nil, { width = w })
|
||||||
|
kids[#kids + 1] = space(0, 18 + parts_h)
|
||||||
|
return col({ width = w }, kids)
|
||||||
end
|
end
|
||||||
local ok = n.canAfford
|
local ok = n.canAfford
|
||||||
local kids = {
|
-- The label that fits the column: "Upgrade · ₥1,100", else "Up · ₥1,100".
|
||||||
fg.button(parent, "Upgrade to T" .. n.tier, { small = small, price = n.price, disabled = not ok }, function(b, nodes)
|
local label = "Upgrade"
|
||||||
if not ok then
|
if fg.button_w(parent, label, { small = true, price = n.price }) > w then
|
||||||
tooltip.add(b, n.blockedText)
|
label = "Up"
|
||||||
else
|
end
|
||||||
|
kids[#kids + 1] = fg.button(parent, label, { small = true, price = n.price, disabled = not ok }, function(b, nodes)
|
||||||
|
tooltip.add(b, n.label .. ((not ok and (n.blockedText or "") ~= "") and (": " .. n.blockedText) or ""))
|
||||||
|
if ok then
|
||||||
cards.pulse(nodes)
|
cards.pulse(nodes)
|
||||||
H.action(self, b, nodes, "upgrade", { tool = t.id })
|
H.action(self, b, nodes, "upgrade", { tool = t.id, track = tr.kind })
|
||||||
end
|
end
|
||||||
end, { width = w }),
|
end, { width = w })
|
||||||
space(0, 4),
|
-- Credits short: the server's shortText (right-aligned, like the store's); else blank.
|
||||||
}
|
local why = n.blocked == "credits" and (n.shortText or "") or ""
|
||||||
-- Credits short: the server's shortText under the button (parts short: the tooltip only).
|
kids[#kids + 1] = made({ height = 18, shrink = 0 }, function(p, x, y, bw)
|
||||||
local why = n.blocked ~= "" and (n.shortText or "") or ""
|
|
||||||
if why ~= "" then
|
if why ~= "" then
|
||||||
local style = { font = "sans", px = 12, color = "danger" }
|
store.need_text(p, why, x + bw, y + 9)
|
||||||
kids[#kids + 1] = fg.measured(parent, function(p, x, y, bw)
|
|
||||||
local probe = ui.rect(p, 0, 0, 1, 1, "none")
|
|
||||||
local lh = cards.para(probe, why, style, 0, 0, bw)
|
|
||||||
gui.delete_node(probe)
|
|
||||||
if lh <= 18 then
|
|
||||||
store.need_text(p, why, x + bw, y + 8)
|
|
||||||
else
|
|
||||||
cards.para(p, why, style, x, y, bw)
|
|
||||||
end
|
end
|
||||||
return math.max(18, lh + 2)
|
|
||||||
end)
|
end)
|
||||||
elseif small then
|
kids[#kids + 1] = made({ height = parts_h, shrink = 0 }, function(p, x, y, bw)
|
||||||
-- A card keeps the need line's place (like the store's): the buttons in a row of
|
cards.parts(st, p, n.parts, x, y, bw)
|
||||||
-- cards line up, and a card doesn't change size when credits come and go.
|
|
||||||
kids[#kids + 1] = space(0, 18)
|
|
||||||
end
|
|
||||||
if #(n.parts or {}) > 0 then
|
|
||||||
kids[#kids + 1] = space(0, 2)
|
|
||||||
kids[#kids + 1] = fg.measured(parent, function(p, x, y, bw)
|
|
||||||
return cards.parts(st, p, n.parts, x, y, bw)
|
|
||||||
end)
|
end)
|
||||||
end
|
|
||||||
return col({ width = w }, kids)
|
return col({ width = w }, kids)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Both tracks side by side, w wide (8 apart). A tree piece.
|
||||||
|
local function tracks_box(self, H, st, parent, t, w)
|
||||||
|
local cw = math.floor((w - 8) / 2)
|
||||||
|
local kids = {}
|
||||||
|
for i, tr in ipairs(t.tracks or {}) do
|
||||||
|
if i > 1 then
|
||||||
|
kids[#kids + 1] = space(w - cw * 2)
|
||||||
|
end
|
||||||
|
kids[#kids + 1] = track_box(self, H, st, parent, t, tr, cw)
|
||||||
|
end
|
||||||
|
return row({ width = w, align = "start" }, kids)
|
||||||
|
end
|
||||||
|
|
||||||
-- The same at (x, y) (a card's footer); returns the height used.
|
-- The same at (x, y) (a card's footer); returns the height used.
|
||||||
local function upgrade(self, H, st, parent, t, x, y, w, small)
|
local function tracks(self, H, st, parent, t, x, y, w)
|
||||||
local tree = upgrade_box(self, H, st, parent, t, w, small)
|
local tree = tracks_box(self, H, st, parent, t, w)
|
||||||
fg.build(flex.solve(tree, x, y), parent)
|
fg.build(flex.solve(tree, x, y), parent)
|
||||||
return tree.h
|
return tree.h
|
||||||
end
|
end
|
||||||
@@ -169,7 +195,8 @@ end
|
|||||||
|
|
||||||
local function tool_card(self, H, st, c, t, open)
|
local function tool_card(self, H, st, c, t, open)
|
||||||
c:image(t.image)
|
c:image(t.image)
|
||||||
-- Name, ×count, tier chip on the right.
|
-- Name, ×count, levels chip on the right (none for a base tool).
|
||||||
|
local tag = ui.levels_tag(t)
|
||||||
c:block(22, function(p, bw)
|
c:block(22, function(p, bw)
|
||||||
local cnt = t.count > 1 and ("×" .. t.count) or nil
|
local cnt = t.count > 1 and ("×" .. t.count) or nil
|
||||||
fg.build(flex.solve(row({ width = bw, height = 16, align = "center" }, {
|
fg.build(flex.solve(row({ width = bw, height = 16, align = "center" }, {
|
||||||
@@ -177,13 +204,13 @@ local function tool_card(self, H, st, c, t, open)
|
|||||||
cnt and space(6) or nil,
|
cnt and space(6) or nil,
|
||||||
cnt and fg.label(p, cnt, { font = "mono", px = 13, color = "muted" }, 16) or nil,
|
cnt and fg.label(p, cnt, { font = "mono", px = 13, color = "muted" }, 16) or nil,
|
||||||
fg.fill(), space(8),
|
fg.fill(), space(8),
|
||||||
fg.inline(p, 16, function(q, x, y, probing)
|
tag ~= "" and fg.inline(p, 16, function(q, x, y, probing)
|
||||||
local tier_chip, cwid = chip.new(q, "T" .. t.tier .. "/" .. t.maxTier, x, y, { font = "mono", px = 11, text = "text_2", pad = 5 })
|
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
|
if not probing then
|
||||||
tooltip.add(tier_chip, "Tier")
|
tooltip.add(lv_chip, "Yield level " .. (t.yieldLevel or 0) .. ", cycle level " .. (t.cycleLevel or 0))
|
||||||
end
|
end
|
||||||
return cwid
|
return cwid
|
||||||
end),
|
end) or nil,
|
||||||
}), 0, 0), p)
|
}), 0, 0), p)
|
||||||
end)
|
end)
|
||||||
c:block(t.reach > 1 and 22 or 28, function(p, bw)
|
c:block(t.reach > 1 and 22 or 28, function(p, bw)
|
||||||
@@ -212,29 +239,12 @@ local function tool_card(self, H, st, c, t, open)
|
|||||||
H.navigate(self, drill_view(self, t.id))
|
H.navigate(self, drill_view(self, t.id))
|
||||||
end
|
end
|
||||||
if open then
|
if open then
|
||||||
-- Attachments: slot pips over the image's corner, tags under the stats.
|
-- The two upgrade tracks: measured on a probe; the footer sits at the row's bottom.
|
||||||
local holder = ui.rect(c.root, 6, 6, 1, 22, "panel")
|
|
||||||
local used = t.slots - t.freeSlots
|
|
||||||
local pw = cards.pips(holder, used, t.slots, 6, 11)
|
|
||||||
gui.set_size(holder, vec(pw + 10, 22))
|
|
||||||
gui.set_color(holder, ui.color("bg", 0.82))
|
|
||||||
tooltip.add(holder, "Attachment slots")
|
|
||||||
if #(t.gear or {}) > 0 then
|
|
||||||
c:block(24, function(p)
|
|
||||||
local gx = 0
|
|
||||||
for _, g in ipairs(t.gear) do
|
|
||||||
local gw, box = cards.tag(p, g.name, gx, 9)
|
|
||||||
tooltip.add(box, g.effectText or cards.effect(g.effect, "attachment"))
|
|
||||||
gx = gx + gw + 6
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
-- Measure the upgrade block on a probe; the footer sits at the row's bottom.
|
|
||||||
local probe = ui.rect(c.root, 0, 0, 1, 1, "none")
|
local probe = ui.rect(c.root, 0, 0, 1, 1, "none")
|
||||||
local fh = upgrade(self, { action = function() end }, st, probe, t, 0, 0, c.bw, true)
|
local fh = tracks(self, { action = function() end }, st, probe, t, 0, 0, c.bw)
|
||||||
gui.delete_node(probe)
|
gui.delete_node(probe)
|
||||||
c:footer(fh, function(f, bw)
|
c:footer(fh + 2, function(f, bw)
|
||||||
upgrade(self, H, st, f, t, 0, 0, bw, true)
|
tracks(self, H, st, f, t, 0, 2, bw)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
c.selected = t.selected and self.view.drill ~= ""
|
c.selected = t.selected and self.view.drill ~= ""
|
||||||
@@ -248,7 +258,7 @@ function M.build(self, root, px, py, w, h, H)
|
|||||||
local c = area.content
|
local c = area.content
|
||||||
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 " Pick one to equip attachments." or ""),
|
fg.label(c, ui.plural(#st.tools, "tool") .. "." .. (open and " Each drill has two upgrade tracks: more per cycle, or shorter cycles." or ""),
|
||||||
{ font = "sans", px = 15, color = "muted" }, 12),
|
{ font = "sans", px = 15, color = "muted" }, 12),
|
||||||
space(0, 18),
|
space(0, 18),
|
||||||
open and fg.button(c, "Buy tools", { small = true }, function(b)
|
open and fg.button(c, "Buy tools", { small = true }, function(b)
|
||||||
@@ -293,31 +303,49 @@ end
|
|||||||
|
|
||||||
local TIER_HEAD = { font = "sans_semi", px = 10, color = "muted", tracking = 0.1, upper = true }
|
local TIER_HEAD = { font = "sans_semi", px = 10, color = "muted", tracking = 0.1, upper = true }
|
||||||
|
|
||||||
-- The tier ladder: a grid (Tier, Yield, Reach, Cycle at 0, 20, 42, 66 % of the row's
|
-- A track's ladder: a grid (Level, value, price and parts at 0, 16, 40 % of the row's
|
||||||
-- inner width, ₥/h at the right), the current tier and the next lit.
|
-- inner width, ₥/h at the right), the current level and the next lit.
|
||||||
local function ladder(st, c, d, inner)
|
local function ladder(st, c, tr, inner)
|
||||||
local fr = { 0, 0.2, 0.42, 0.66, 1 }
|
local fr = { 0, 0.16, 0.4, 0.84 }
|
||||||
local cols = {}
|
local cols = {}
|
||||||
for i = 1, 4 do
|
for i = 1, 3 do
|
||||||
cols[i] = math.floor((inner - 16) * fr[i + 1]) - math.floor((inner - 16) * fr[i])
|
cols[i] = math.floor((inner - 16) * fr[i + 1]) - math.floor((inner - 16) * fr[i])
|
||||||
end
|
end
|
||||||
cols[5] = "1fr"
|
cols[4] = "1fr"
|
||||||
local heads = {}
|
local heads = {}
|
||||||
for i, lbl in ipairs({ "Tier", "Yield", "Reach", "Cycle" }) do
|
for i, lbl in ipairs({ "Level", tr.kind == "cycle" and "Cycle" or "Yield", "Price" }) do
|
||||||
heads[i] = fg.label(c, lbl, TIER_HEAD, 12)
|
heads[i] = fg.label(c, lbl, TIER_HEAD, 12)
|
||||||
end
|
end
|
||||||
local head = box({ columns = cols, height = 14, padding = { 0, 8, 0, 8 }, align = "start" }, heads)
|
local head = box({ columns = cols, height = 14, padding = { 0, 8, 0, 8 }, align = "start" }, heads)
|
||||||
-- (₥/h: its left edge 22 before the rate column's right edge)
|
-- (₥/h: its left edge 22 before the rate column's right edge)
|
||||||
local rate_head = fg.label(c, theme.currency .. "/h", TIER_HEAD, 12, { position = "absolute", top = 0, left = inner - 30 })
|
local rate_head = fg.label(c, theme.currency .. "/h", TIER_HEAD, 12, { position = "absolute", top = 0, left = inner - 30 })
|
||||||
local rows = {}
|
local rows = {}
|
||||||
for _, r in ipairs(d.ladder) do
|
for _, r in ipairs(tr.ladder or {}) do
|
||||||
local color = r.current and "text" or (r.next and "stat_up" or "text_2")
|
local color = r.current and "text" or (r.next and "stat_up" or "text_2")
|
||||||
local font = r.next and "mono_semi" or "mono"
|
local font = r.next and "mono_semi" or "mono"
|
||||||
local kids = {}
|
local price = "—"
|
||||||
for i, v in ipairs({ "T" .. r.tier, ui.money(r.yield), cards.wells(st, r.reach), ui.cycle(r.cycleSecs) }) do
|
local full
|
||||||
kids[i] = fg.label(c, v, { font = font, px = 12, color = color }, 20)
|
if (r.price or 0) > 0 then
|
||||||
|
price = theme.currency .. cards.compact(r.price)
|
||||||
|
local parts = {}
|
||||||
|
for _, pt in ipairs(r.parts or {}) do
|
||||||
|
parts[#parts + 1] = ui.money(pt.need) .. " " .. pt.name
|
||||||
end
|
end
|
||||||
kids[5] = fg.label(c, cards.compact(r.rate), { font = font, px = 12, color = r.next and "stat_up" or "muted" }, 20, { justify_self = "end" })
|
if #parts > 0 then
|
||||||
|
price = price .. " + " .. table.concat(parts, ", ")
|
||||||
|
full = theme.currency .. ui.money(r.price) .. " + " .. table.concat(parts, ", ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local pl = fg.label(c, price, { font = font, px = 12, color = color }, 20, { fit = true, shrink = 1 })
|
||||||
|
if full then
|
||||||
|
pl.done = function(n) tooltip.add(n, full) end
|
||||||
|
end
|
||||||
|
local kids = {
|
||||||
|
fg.label(c, tostring(r.level), { font = font, px = 12, color = color }, 20),
|
||||||
|
fg.label(c, track_value(tr, r.value), { font = font, px = 12, color = color }, 20),
|
||||||
|
pl,
|
||||||
|
fg.label(c, cards.compact(r.rate), { font = font, px = 12, color = r.next and "stat_up" or "muted" }, 20, { justify_self = "end" }),
|
||||||
|
}
|
||||||
local lit = r.current and "cyan" or (r.next and "stat_up") or nil
|
local lit = r.current and "cyan" or (r.next and "stat_up") or nil
|
||||||
rows[#rows + 1] = box({ columns = cols, rows = { 20 }, padding = { 0, 8, 0, 8 } }, kids, { make = lit and function(p, x, y, w)
|
rows[#rows + 1] = box({ columns = cols, rows = { 20 }, padding = { 0, 8, 0, 8 } }, kids, { make = lit and function(p, x, y, w)
|
||||||
ui.rect(p, x, y, w, 20, lit, 0.08)
|
ui.rect(p, x, y, w, 20, lit, 0.08)
|
||||||
@@ -346,7 +374,7 @@ function M.aside(self, root, w, h, H)
|
|||||||
if not t then
|
if not t then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local open = st.unlocked.store -- upgrade, attachments, selling (onboarding.go)
|
local open = st.unlocked.store -- upgrades, selling (onboarding.go)
|
||||||
local pad = 18
|
local pad = 18
|
||||||
local inner = w - pad * 2
|
local inner = w - pad * 2
|
||||||
local area = scroll.area(self, root, 0, 2, w, h - 4, "drill")
|
local area = scroll.area(self, root, 0, 2, w, h - 4, "drill")
|
||||||
@@ -356,7 +384,7 @@ function M.aside(self, root, w, h, H)
|
|||||||
kids[#kids + 1] = b
|
kids[#kids + 1] = b
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Head: image, name, tier (and "Mining now"), close.
|
-- Head: image, name, track levels (and "Mining now"), close.
|
||||||
local img = 84
|
local img = 84
|
||||||
local tw = inner - img - 14
|
local tw = inner - img - 14
|
||||||
add(row({ height = img }, {
|
add(row({ height = img }, {
|
||||||
@@ -368,10 +396,12 @@ function M.aside(self, root, w, h, H)
|
|||||||
space(14),
|
space(14),
|
||||||
col({ grow = 1, shrink = 1 }, {
|
col({ grow = 1, shrink = 1 }, {
|
||||||
fg.label(c, t.name, "h2", 28, { fit = true, shrink = 1, max_width = tw - 40 }),
|
fg.label(c, t.name, "h2", 28, { fit = true, shrink = 1, max_width = tw - 40 }),
|
||||||
fg.label(c, "T" .. t.tier, { font = "mono", px = 13, color = "muted" }, 20),
|
-- Its levels (" Y2 C1" without the space), or a line saying it has none (the row keeps its height).
|
||||||
|
fg.label(c, ui.levels_tag(t) ~= "" and ui.levels_tag(t):sub(2) or "No upgrades yet",
|
||||||
|
{ font = "mono", px = 13, color = "muted" }, 20),
|
||||||
d.mining and space(0, 4) or nil,
|
d.mining and space(0, 4) or nil,
|
||||||
d.mining and fg.measured(c, function(p, x, y, bw)
|
d.mining and fg.measured(c, function(p, x, y, bw)
|
||||||
return cards.para(p, open and "Mining now. Changes apply from its next drill." or "Mining now.", { font = "sans", px = 13, color = "muted" }, x, y, bw)
|
return cards.para(p, open and "Mining now. Upgrades apply from its next drill." or "Mining now.", { font = "sans", px = 13, color = "muted" }, x, y, bw)
|
||||||
end) or nil,
|
end) or nil,
|
||||||
}),
|
}),
|
||||||
fg.button(c, "×", { small = true, w = 32 }, function(b)
|
fg.button(c, "×", { small = true, w = 32 }, function(b)
|
||||||
@@ -380,30 +410,36 @@ function M.aside(self, root, w, h, H)
|
|||||||
}))
|
}))
|
||||||
add(space(0, 14))
|
add(space(0, 14))
|
||||||
|
|
||||||
-- Stats and the upgrade.
|
-- Stats and the two tracks.
|
||||||
add(col({ padding = { 4, 0, 6, 0 } }, { rule(), stats_box(st, c, t) }))
|
add(col({ padding = { 4, 0, 6, 0 } }, { rule(), stats_box(st, c, t) }))
|
||||||
local u = open and t.preview
|
|
||||||
if u and u.rateTo > u.rateFrom + 0.5 then
|
|
||||||
-- "Income ₥1k/h ▸ ₥2k/h · pays back in 5m": the fonts have no arrow, cards.arrow draws it.
|
|
||||||
local m12 = { font = "mono", px = 12, color = "muted" }
|
|
||||||
add(row({ height = 16, align = "center" }, {
|
|
||||||
fg.label(c, "Income " .. theme.currency .. cards.compact(u.rateFrom) .. "/h", m12, 16), space(5),
|
|
||||||
made({ width = 13, height = 16, shrink = 0 }, function(p, x, y) cards.arrow(p, x, y + 8) end),
|
|
||||||
fg.label(c, theme.currency .. cards.compact(u.rateTo) .. "/h"
|
|
||||||
.. (u.paybackSecs and (" · pays back in " .. cards.span(u.paybackSecs)) or ""), m12, 16),
|
|
||||||
}))
|
|
||||||
add(space(0, 6))
|
|
||||||
end
|
|
||||||
if open then
|
if open then
|
||||||
add(space(0, 4))
|
add(space(0, 4))
|
||||||
add(upgrade_box(self, H, st, c, t, inner, false))
|
add(tracks_box(self, H, st, c, t, inner))
|
||||||
end
|
end
|
||||||
add(space(0, 8))
|
add(space(0, 8))
|
||||||
|
|
||||||
-- Tier ladder.
|
-- Each track's ladder, with what its next level adds per hour.
|
||||||
if #(d.ladder or {}) > 0 then
|
local m12 = { font = "mono", px = 12, color = "muted" }
|
||||||
add(h3(c, "Tiers"))
|
for _, tr in ipairs(t.tracks or {}) do
|
||||||
add(ladder(st, c, d, inner))
|
add(row({ height = 20, align = "center" }, {
|
||||||
|
fg.label(c, tr.name, "h3", 12), space(8),
|
||||||
|
fg.label(c, tr.kind == "cycle" and "cycle" or "yield per cycle", { font = "sans", px = 12, color = "muted" }, 12),
|
||||||
|
}))
|
||||||
|
add(space(0, 6))
|
||||||
|
local n = open and tr.next
|
||||||
|
if n and n.rateTo > n.rateFrom + 0.5 then
|
||||||
|
-- "Next level: ₥1k/h ▸ ₥2k/h · pays back in 5m": the fonts have no arrow, cards.arrow draws it.
|
||||||
|
add(row({ height = 16, align = "center" }, {
|
||||||
|
fg.label(c, "Next level: " .. theme.currency .. cards.compact(n.rateFrom) .. "/h", m12, 16), space(5),
|
||||||
|
made({ width = 13, height = 16, shrink = 0 }, function(p, x, y) cards.arrow(p, x, y + 8) end),
|
||||||
|
fg.label(c, theme.currency .. cards.compact(n.rateTo) .. "/h"
|
||||||
|
.. (n.paybackSecs and (" · pays back in " .. cards.span(n.paybackSecs)) or ""), m12, 16, { fit = true, shrink = 1 }),
|
||||||
|
}))
|
||||||
|
add(space(0, 6))
|
||||||
|
elseif open then
|
||||||
|
add(space(0, 22)) -- (the line's room, kept at the top level: Sell drill stays put)
|
||||||
|
end
|
||||||
|
add(ladder(st, c, tr, inner))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Idle copies while this one mines: deploy one on its best well.
|
-- Idle copies while this one mines: deploy one on its best well.
|
||||||
@@ -417,12 +453,12 @@ function M.aside(self, root, w, h, H)
|
|||||||
gui.delete_node(probe.node)
|
gui.delete_node(probe.node)
|
||||||
add(col({ height = 60 }, {
|
add(col({ height = 60 }, {
|
||||||
row({ height = 20, align = "center" }, {
|
row({ height = 20, align = "center" }, {
|
||||||
-- "N idle <tool>" opens that copy's panel (the web's IdleURL), then its tier.
|
-- "N idle <tool>" opens that copy's panel (the web's IdleURL), then its levels.
|
||||||
made({ width = lw, height = 20, shrink = 0 }, function(p, x, y)
|
made({ width = lw, height = 20, shrink = 0 }, function(p, x, y)
|
||||||
H.text_link(self, p, link, lstyle, x, y + 10, drill_view(self, d.idleBest), "idle-copy")
|
H.text_link(self, p, link, lstyle, x, y + 10, drill_view(self, d.idleBest), "idle-copy")
|
||||||
end),
|
end),
|
||||||
space(6),
|
space(6),
|
||||||
fg.label(c, "T" .. (copy and copy.tier or t.tier), { font = "mono", px = 12, color = "muted" }, 20),
|
fg.label(c, ui.levels_tag(copy or t):sub(2), { font = "mono", px = 12, color = "muted" }, 20),
|
||||||
}),
|
}),
|
||||||
fg.label(c, to and ("Best well: " .. to.resource .. " on " .. to.moonName) or "No well to deploy on", "small", 16),
|
fg.label(c, to and ("Best well: " .. to.resource .. " on " .. to.moonName) or "No well to deploy on", "small", 16),
|
||||||
to and fg.button(c, "Deploy one", { small = true, primary = true }, function(b, nodes)
|
to and fg.button(c, "Deploy one", { small = true, primary = true }, function(b, nodes)
|
||||||
@@ -431,118 +467,12 @@ function M.aside(self, root, w, h, H)
|
|||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The Store's parts: slots, spare attachments, selling the drill.
|
-- The Store's part: selling the drill.
|
||||||
if open then
|
if open then
|
||||||
-- Slots.
|
|
||||||
add(h3(c, "Slots"))
|
|
||||||
local slots = {}
|
|
||||||
for _, sl in ipairs(d.slots) do
|
|
||||||
local rest
|
|
||||||
if sl.attachment ~= "" then
|
|
||||||
local a = catalog_attachment(st, sl.attachment)
|
|
||||||
rest = {
|
|
||||||
col({ padding_top = 5, shrink = 0 }, {
|
|
||||||
made({ width = 32, height = 32 }, function(p, x, y)
|
|
||||||
cards.image(ui.rect(p, x, y, 32, 32, "bg"), a.image, 0, 0, 32)
|
|
||||||
end),
|
|
||||||
}),
|
|
||||||
space(8),
|
|
||||||
col({ grow = 1, shrink = 1, padding_top = 2 }, {
|
|
||||||
fg.label(c, sl.attachment, { font = "sans", px = 15, color = "text" }, 20),
|
|
||||||
fg.label(c, a.effectText or cards.effect(a.effect, "attachment"), { font = "mono", px = 12, color = "violet_hi" }, 16),
|
|
||||||
}),
|
|
||||||
col({ padding_top = 5 }, {
|
|
||||||
fg.button(c, "Unequip", { small = true }, function(b, nodes)
|
|
||||||
H.action(self, b, nodes, "unequip", { item = sl.attachment, to = d.toolId })
|
|
||||||
end),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
else
|
|
||||||
rest = { space(4), row({ align = "center" }, { fg.label(c, "Empty", { font = "sans", px = 15, color = "muted" }, 16) }) }
|
|
||||||
end
|
|
||||||
local r = row({ height = 42 }, {
|
|
||||||
rule(),
|
|
||||||
row({ width = 18, padding_left = 2, align = "center", shrink = 0 }, { fg.label(c, sl.n, { font = "mono", px = 12, color = "muted" }, 16) }),
|
|
||||||
})
|
|
||||||
for _, k in ipairs(rest) do
|
|
||||||
r.children[#r.children + 1] = k
|
|
||||||
end
|
|
||||||
slots[#slots + 1] = r
|
|
||||||
end
|
|
||||||
add(col({}, slots))
|
|
||||||
add(space(0, 8))
|
|
||||||
|
|
||||||
-- Spare attachments: equip (with the stats it'd give), sell one.
|
|
||||||
add(h3(c, "Spare attachments"))
|
|
||||||
if #d.spares == 0 then
|
|
||||||
add(row({ height = 40, align = "start" }, {
|
|
||||||
row({ grow = 1, height = S.button_sm_h, align = "center" }, { fg.label(c, "No spare attachments.", "small", 16) }),
|
|
||||||
fg.button(c, "Buy some in the store", { small = true }, function(b)
|
|
||||||
H.link(self, b, { section = "store", moon = self.view.moon })
|
|
||||||
end),
|
|
||||||
}))
|
|
||||||
end
|
|
||||||
local m12 = function(color) return { font = "mono", px = 12, color = color } end
|
|
||||||
for _, sp in ipairs(d.spares) do
|
|
||||||
local a = catalog_attachment(st, sp.name)
|
|
||||||
local sw = fg.button_w(c, "Sell", { small = true, variant = "danger" })
|
|
||||||
-- Sell asks in place (cards.ask_button): the question where the text was, Confirm
|
|
||||||
-- where Equip was, Cancel on Sell's spot.
|
|
||||||
local sell_id = "confirm:sell-attachment" .. (#d.spares > 1 and (":" .. sp.name) or "")
|
|
||||||
local asking = cards.asking(self, sell_id, sp.confirm, { item = sp.name })
|
|
||||||
local ask_ok, ask_cancel
|
|
||||||
if asking then
|
|
||||||
ask_ok, ask_cancel = cards.ask_buttons(self, c, sp.confirm, sw)
|
|
||||||
end
|
|
||||||
add(row({ height = sp.canEquip and 50 or 58 }, {
|
|
||||||
rule(),
|
|
||||||
col({ padding_top = 6, shrink = 0 }, {
|
|
||||||
made({ width = 36, height = 36 }, function(p, x, y)
|
|
||||||
cards.image(ui.rect(p, x, y, 36, 36, "bg"), a.image, 0, 0, 36)
|
|
||||||
end),
|
|
||||||
}),
|
|
||||||
space(10),
|
|
||||||
asking and col({ grow = 1, shrink = 1, basis = 0, padding_top = 6, padding_right = 8 }, { cards.ask_text(sp.confirm, c) }) or nil,
|
|
||||||
asking and row({ gap = 6, padding_top = 8, align = "start", shrink = 0 }, { ask_ok, ask_cancel }) or nil,
|
|
||||||
(not asking) and col({ grow = 1, shrink = 1, padding_top = 6 }, {
|
|
||||||
row({ height = 16, align = "center" }, {
|
|
||||||
fg.label(c, sp.name, { font = "sans", px = 15, color = "text" }, 16),
|
|
||||||
sp.count > 1 and space(6) or nil,
|
|
||||||
sp.count > 1 and fg.label(c, "×" .. sp.count, m12("muted"), 16) or nil,
|
|
||||||
}),
|
|
||||||
space(0, 2),
|
|
||||||
-- What the drill would do with it: effect, then cycle and yield (stat_up where better).
|
|
||||||
row({ height = 16, align = "center" }, {
|
|
||||||
fg.label(c, a.effectText or cards.effect(a.effect, "attachment"), m12("violet_hi"), 16), space(6),
|
|
||||||
fg.label(c, "· then", m12("muted"), 16), space(5),
|
|
||||||
fg.label(c, ui.cycle(sp.cycleSecs), m12(sp.cycleSecs < t.cycleSecs and "stat_up" or "text_2"), 16),
|
|
||||||
fg.label(c, ", +" .. ui.money(sp.yield), m12(sp.yield > t.yield + 1e-9 and "stat_up" or "text_2"), 16),
|
|
||||||
}),
|
|
||||||
}) or nil,
|
|
||||||
(not asking) and row({ gap = 6, padding_top = 8, align = "start" }, {
|
|
||||||
fg.button(c, "Equip", { small = true, primary = true, disabled = not sp.canEquip }, function(b, nodes)
|
|
||||||
if sp.canEquip then
|
|
||||||
H.action(self, b, nodes, "equip", { item = sp.name, to = d.toolId })
|
|
||||||
else
|
|
||||||
tooltip.add(b, sp.whyText)
|
|
||||||
end
|
|
||||||
end),
|
|
||||||
fg.button(c, "Sell", { small = true, variant = "danger" }, function(b)
|
|
||||||
tooltip.add(b, "Sell one for " .. theme.currency .. ui.money(sp.resale))
|
|
||||||
cards.ask_button(self, b, sp.confirm, "sell-attachment", { item = sp.name }, sell_id)
|
|
||||||
end),
|
|
||||||
}) or nil,
|
|
||||||
not asking and not sp.canEquip and fg.label(c, sp.whyText, { font = "sans", px = 11, color = "muted" }, 16,
|
|
||||||
{ position = "absolute", top = 38, right = sw + 6 }) or nil,
|
|
||||||
}))
|
|
||||||
end
|
|
||||||
add(space(0, 8))
|
|
||||||
|
|
||||||
-- Sell the drill.
|
-- Sell the drill.
|
||||||
add(h3(c, "Sell drill"))
|
add(h3(c, "Sell drill"))
|
||||||
local why = d.noSale ~= "" and d.noSaleText or nil
|
local why = d.noSale ~= "" and d.noSaleText or nil
|
||||||
local note = "Part of what it and its upgrades cost. Refined parts aren't refunded"
|
local note = "Part of what it and its upgrades cost. Refined parts aren't refunded."
|
||||||
.. (#(d.gear or {}) > 0 and "; its attachments go back to your inventory." or ".")
|
|
||||||
-- Sell asks in place (cards.ask_button): the question under the price instead of the
|
-- Sell asks in place (cards.ask_button): the question under the price instead of the
|
||||||
-- note, Confirm left of the Sell button's spot, Cancel on it.
|
-- note, Confirm left of the Sell button's spot, Cancel on it.
|
||||||
local asking = not why and cards.asking(self, "confirm:sell-tool", d.confirm, { tool = d.toolId })
|
local asking = not why and cards.asking(self, "confirm:sell-tool", d.confirm, { tool = d.toolId })
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-- Selling a moon (state.moon.sale, the web's "moon-sale" panel; here at the foot of the
|
-- Selling a moon (state.moon.sale, the web's "moon-sale" panel; here at the foot of the
|
||||||
-- aside's well panel, aside.lua):
|
-- Moons section's main panel, moons.lua):
|
||||||
-- the offer and how it compares with a new moon, the Sell moon button (asks first, in
|
-- the offer and how it compares with a new moon, the Sell moon button (asks first, in
|
||||||
-- place with the server's question: cards.ask_button; why it can't be sold yet is its tooltip), and on demand the
|
-- place with the server's question: cards.ask_button; why it can't be sold yet is its tooltip), and on demand the
|
||||||
-- note (with the hold countdown, "Sellable in") and the lines: what's left in each well at
|
-- note (with the hold countdown, "Sellable in") and the lines: what's left in each well at
|
||||||
@@ -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 (the aside puts it under the selected well).
|
-- The panel, w wide: a tree piece (moons.lua puts it under the idle drills).
|
||||||
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 {}
|
||||||
|
|||||||
+467
-210
@@ -1,8 +1,9 @@
|
|||||||
-- The Moons section (main panel): the selected moon (id, name, class and rating stars,
|
-- The Moons section (main panel): the selected moon (id, name, class and rating stars,
|
||||||
-- description, Claim), its wells table (supply bars, drill timer bars, claim / countdown /
|
-- description, Claim N drills), its wells as cards with every action on a well (deploy,
|
||||||
-- redrill per well, the multi-well row); a mined-out
|
-- claim, claim and redeploy, redrill: "well cards" below), the multi-well card, the tray
|
||||||
-- moon's summary; the first-moon pitch when there is none. Also the well helpers the
|
-- of idle drills and selling the moon; a mined-out moon's summary; the first-moon pitch
|
||||||
-- aside's well panel shares (supply_bar, supply_max, tool_name).
|
-- when there is none. The aside only shows facts (aside.lua). Also the well helpers the
|
||||||
|
-- aside shares (supply_bar, supply_max, tool_name, short_name).
|
||||||
|
|
||||||
local theme = require("modules.theme")
|
local theme = require("modules.theme")
|
||||||
local ui = require("modules.ui")
|
local ui = require("modules.ui")
|
||||||
@@ -15,6 +16,10 @@ local scroll = require("components.scroll")
|
|||||||
local ready = require("screens.game.core.ready")
|
local ready = require("screens.game.core.ready")
|
||||||
local flex = require("modules.flex")
|
local flex = require("modules.flex")
|
||||||
local fg = require("modules.flex_gui")
|
local fg = require("modules.flex_gui")
|
||||||
|
local images = require("modules.images")
|
||||||
|
local test = require("modules.test")
|
||||||
|
local predict = require("modules.predict")
|
||||||
|
local moon_sale = require("screens.game.moon_sale")
|
||||||
|
|
||||||
local S = theme.size
|
local S = theme.size
|
||||||
|
|
||||||
@@ -100,10 +105,10 @@ function M.track_drills(g)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- A tool's name and tier by id.
|
-- A tool's name and its levels tag (ui.levels_tag: " Y2 C1", "" for a base tool) by id.
|
||||||
function M.tool_name(st, id)
|
function M.tool_name(st, id)
|
||||||
local t = find(st.tools, id)
|
local t = find(st.tools, id)
|
||||||
return t and t.name or "Tool", t and t.tier or 0
|
return t and t.name or "Tool", ui.levels_tag(t)
|
||||||
end
|
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
|
||||||
@@ -169,219 +174,459 @@ local function past_moon(g, root, px, py, inner, moon, H)
|
|||||||
})
|
})
|
||||||
fg.build(flex.solve(tree, px, py), root)
|
fg.build(flex.solve(tree, px, py), root)
|
||||||
end
|
end
|
||||||
-- Timer bars for a drill queue, oldest first (the web's drill-stack): thin bars 40 wide
|
|
||||||
-- stacked, filling while it runs, full when ready, tinted by what it gets (ok, short,
|
---------------------------------------------------------------- well cards
|
||||||
-- none); a multi-well drill has a diamond cap. At most 4 bars, then "+N" beside them.
|
-- The wells as cards (Drilling B · Well cards): every action on a well is on its card,
|
||||||
-- row: the row's key (a multi-well drill has a bar on every row it reaches). y is the
|
-- the aside only shows. A card: the resource and its supply, the supply bar and a line
|
||||||
-- stack's middle.
|
-- of what the drills leave, then a stack of slots, all SLOT_H tall: the deploy slot
|
||||||
local MAX_BARS, BAR_W, BAR_H, BAR_GAP = 4, 40, 3, 2
|
-- first (it keeps its spot while you deploy again and again: new drills go under it),
|
||||||
local MORE = { font = "mono", px = 11, color = "muted" }
|
-- then a slot per drill, oldest first (time left and its bar; when it's done, gold with
|
||||||
local function drill_bars(g, parent, x, y, drills, row_key)
|
-- Claim and Redeploy on it), then one slot for the idle drills that last mined the well
|
||||||
local n = math.min(#drills, MAX_BARS)
|
-- (Redrill). Claiming one slot claims every ready drill of the well (claim-well); the
|
||||||
local top = y - math.floor((n * (BAR_H + BAR_GAP) - BAR_GAP) / 2)
|
-- next slot slides into its place. Clicking the card (not a button) selects the well:
|
||||||
for i = 1, n do
|
-- the aside shows its facts.
|
||||||
local d = drills[i]
|
--
|
||||||
local color = d.state == "short" and "ready" or "cyan"
|
-- Deploying: the tray under the cards lists the idle drills (grouped like copies: same
|
||||||
local by = top + (i - 1) * (BAR_H + BAR_GAP)
|
-- name, track levels, yield, cycle and reach). A well's deploy slot sends the drill picked in the
|
||||||
-- Keyed by row and tool (one drill each), so a restarted or corrected drill eases.
|
-- tray (g.deploy_pick), or with none picked the first one-well drill in the tray that
|
||||||
bar.timer(g, parent, x, by, BAR_W, BAR_H, { start = d.start, ends = d["end"], ready = d.ready, color = color,
|
-- has room there (smallest first; predict.room: what it would get after the moon's drills). Picking is
|
||||||
none = d.state == "none", key = "bar:" .. row_key .. ":" .. d.toolId })
|
-- a toggle; the pick is forgotten once that group is used up.
|
||||||
if (d.reach or 1) > 1 then
|
|
||||||
-- The diamond cap (app.css .drill-bar.is-wide::after).
|
local SLOT_H, SLOT_GAP = 60, 6
|
||||||
local cap = ui.rect(parent, 0, 0, 5, 5, d.state == "none" and "muted" or color)
|
local CARD_MIN, CARD_GAP, CARD_PAD = 210, 12, 12
|
||||||
gui.set_pivot(cap, gui.PIVOT_CENTER)
|
local TILE_MIN, TILE_H = 150, 52
|
||||||
gui.set_position(cap, vec(x + BAR_W + 1, -(by + 1)))
|
local SLOT_BTN_H = 26
|
||||||
gui.set_rotation(cap, vmath.quat_rotation_z(math.rad(45)))
|
local EPS = 1e-9
|
||||||
|
|
||||||
|
local NAME13 = { font = "sans_semi", px = 13, color = "text" }
|
||||||
|
local M12 = { font = "mono", px = 12, color = "muted" }
|
||||||
|
local S12 = { font = "sans", px = 12, color = "muted" }
|
||||||
|
local S11 = { font = "sans", px = 11, color = "muted" }
|
||||||
|
|
||||||
|
-- A tool's short name ("PD-1 T0"): its code (else the last word of its name), and its
|
||||||
|
-- track levels when it has any ("PD-1 Y2 C1", ui.levels_tag). t: a state.tools entry
|
||||||
|
-- (nil: "Drill").
|
||||||
|
function M.short_name(t)
|
||||||
|
if not t then
|
||||||
|
return "Drill"
|
||||||
|
end
|
||||||
|
local s = (t.code and t.code ~= "") and t.code or ((t.name or "Drill"):match("(%S+)$") or "Drill")
|
||||||
|
s = s .. ui.levels_tag(t)
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The web's short wording of a reach ("3 wells", "whole moon"; nil for one).
|
||||||
|
local function reach_short(st, n)
|
||||||
|
if not n or n <= 1 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local list = st.catalog and st.catalog.reach or {}
|
||||||
|
local pick = list[#list]
|
||||||
|
for _, r in ipairs(list) do
|
||||||
|
if r.reach == n then
|
||||||
|
pick = r
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return pick and pick.short ~= "" and pick.short or (n .. " wells")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Idle tools grouped like copies (name, levels, yield, cycle, reach; read generically), in
|
||||||
|
-- order of yield, then key: { key, tool (one idle copy), count }.
|
||||||
|
function M.idle_groups(st)
|
||||||
|
local out, by = {}, {}
|
||||||
|
for _, t in ipairs(st.tools or {}) do
|
||||||
|
if not t.stakeId then
|
||||||
|
local k = table.concat({ t.code or t.name or "", ui.levels_tag(t), tostring(t.yield),
|
||||||
|
tostring(t.cycleSecs), tostring(t.reach or 1) }, ":")
|
||||||
|
local grp = by[k]
|
||||||
|
if not grp then
|
||||||
|
grp = { key = k, tool = t, count = 0 }
|
||||||
|
by[k], out[#out + 1] = grp, grp
|
||||||
|
end
|
||||||
|
grp.count = grp.count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- (in a fixed order, not the server's list's: a claimed drill coming back idle mustn't
|
||||||
|
-- shuffle the tiles; smallest yield first, so an unpicked deploy slot sends the
|
||||||
|
-- smallest drill with room)
|
||||||
|
table.sort(out, function(a, b)
|
||||||
|
local ya, yb = a.tool.yield or 0, b.tool.yield or 0
|
||||||
|
if ya ~= yb then
|
||||||
|
return ya < yb
|
||||||
|
end
|
||||||
|
return a.key < b.key
|
||||||
|
end)
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- What a cycle of tool t would get on the well after the moon's drills (predict.room), and
|
||||||
|
-- its full amount there (the yield with the resource's live boost).
|
||||||
|
local function gets_here(st, well, t)
|
||||||
|
local boosts = st.rules and st.rules.boosts and st.rules.boosts.yield or {}
|
||||||
|
local amount = (t.yield or 0) * (boosts[well.resource] or 1)
|
||||||
|
return predict.room(st, well.id, amount, t.reach or 1) or amount, amount
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The group a well's deploy slot sends: the tray's pick (explicit = true, even when it has
|
||||||
|
-- no room there), else the first one-well group with room. Returns group, here, amount,
|
||||||
|
-- explicit.
|
||||||
|
local function deploy_choice(g, st, well, groups)
|
||||||
|
for _, grp in ipairs(groups) do
|
||||||
|
if grp.key == g.deploy_pick then
|
||||||
|
local here, amount = gets_here(st, well, grp.tool)
|
||||||
|
return grp, here, amount, true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, grp in ipairs(groups) do
|
||||||
|
-- (a multi-well drill goes out only when picked: it mines other wells too)
|
||||||
|
local here, amount = 0, 0
|
||||||
|
if (grp.tool.reach or 1) <= 1 then
|
||||||
|
here, amount = gets_here(st, well, grp.tool)
|
||||||
|
end
|
||||||
|
if here > EPS then
|
||||||
|
return grp, here, amount, false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function drill_label(st, drills)
|
-- An action button's test id (core/hooks: "act:<name>?<sorted params>").
|
||||||
if #drills == 0 then
|
local function act_id(name, params)
|
||||||
return ""
|
local keys = {}
|
||||||
elseif #drills == 1 then
|
for k, v in pairs(params) do
|
||||||
local name, tier = M.tool_name(st, drills[1].toolId)
|
keys[#keys + 1] = k .. "=" .. tostring(v)
|
||||||
return name:match("(%S+)$") .. " T" .. tier
|
|
||||||
end
|
end
|
||||||
return ui.plural(#drills, "drill")
|
table.sort(keys)
|
||||||
|
return "act:" .. name .. "?" .. table.concat(keys, "&")
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The drill column of a row, built at its width (52 tall): the stacked timer bars for
|
-- A one-line label box in a slot or tile (cut with … when it doesn't fit).
|
||||||
-- every drill on it, multi-well ones included (the web's Queue), and the label of its
|
local function fit(p, str, style, opts)
|
||||||
-- own drills (DrillLabel) beside them, or under them (after "+N") when there's no room
|
opts = opts or {}
|
||||||
-- (decided at its width). Its tooltip (DrillsTitle): each drill, oldest first, and what
|
opts.fit, opts.shrink = true, opts.shrink or 1
|
||||||
-- it gets. pad: room kept before the status buttons.
|
return fg.label(p, str, style, 16, opts)
|
||||||
local function drill_cell(g, queue, fates, label, row_key, pad)
|
|
||||||
return made({ grow = 1, shrink = 1, padding_right = pad }, function(p, x, y, w, h)
|
|
||||||
local dw = w - pad
|
|
||||||
if #fates > 0 then
|
|
||||||
tooltip.add(ui.rect(p, x, y + 6, math.max(40, dw), h - 12, "none"), table.concat(fates, "\n"))
|
|
||||||
end
|
end
|
||||||
local n = math.min(#queue, MAX_BARS)
|
|
||||||
local stack_h = n * (BAR_H + BAR_GAP)
|
-- The deploy slot (a dashed ghost button SLOT_H tall): "+ Deploy <drill>" and what a
|
||||||
local more = #queue - n
|
-- cycle gets here, or why it can't (no idle drills, no room).
|
||||||
local function bars(beside)
|
local function deploy_slot(g, st, well, groups, H)
|
||||||
local more_l = beside and more > 0 and fg.label(p, "+" .. more, MORE, 16) or nil
|
local grp, here, amount, explicit = deploy_choice(g, st, well, groups)
|
||||||
return row({ shrink = 0, align = "center" }, {
|
local ok = grp ~= nil and here > EPS
|
||||||
made({ width = BAR_W + 4, height = stack_h, shrink = 0 }, function(q, bx, by)
|
return made({ height = SLOT_H, shrink = 0 }, function(p, x, y, w)
|
||||||
drill_bars(g, q, bx, by + math.floor(stack_h / 2), queue, row_key)
|
local b, bn = ui.button(p, "", x, y, { variant = "ghost", w = w, h = SLOT_H, disabled = not ok })
|
||||||
end),
|
local title, sub, sub_color
|
||||||
more_l and space(2) or nil, more_l,
|
if #groups == 0 then
|
||||||
})
|
title, sub = "No idle drills", st.unlocked.store and "Buy one in the Store" or "Every drill is busy"
|
||||||
end
|
elseif not grp then
|
||||||
local tree
|
title, sub = "No room here", "The drills on it take all it has"
|
||||||
if #queue == 0 then
|
elseif not ok then
|
||||||
tree = row({ height = h, align = "center" }, {
|
title, sub = "No room for " .. M.short_name(grp.tool), "Pick another drill below"
|
||||||
fg.label(p, "Empty", { font = "sans", px = 14, color = "muted" }, 16, { fit = true, shrink = 1 }),
|
|
||||||
})
|
|
||||||
else
|
else
|
||||||
local style = { font = "sans", px = 12, color = "text_2" }
|
local t = grp.tool
|
||||||
local probe = fg.label(p, label, style, 16)
|
local reach = reach_short(st, t.reach)
|
||||||
local lw = probe.measure()
|
title = "+ Deploy " .. M.short_name(t)
|
||||||
gui.delete_node(probe.node)
|
sub = "+" .. ui.money(here) .. " / " .. ui.cycle(t.cycleSecs) .. (reach and (" · " .. reach) or "")
|
||||||
if BAR_W + 4 + (more > 0 and 18 or 0) + 6 + lw <= dw then
|
if here < amount - EPS then
|
||||||
tree = row({ height = h, align = "center" }, {
|
sub, sub_color = "Only +" .. ui.money(here) .. " left here (of " .. ui.money(amount) .. ")", "ready"
|
||||||
bars(true), space(6), fg.label(p, label, style, 16, { fit = true, shrink = 1 }),
|
|
||||||
})
|
|
||||||
else
|
|
||||||
-- (narrow: "+N" leads the label under the bars)
|
|
||||||
tree = col({ height = h, padding_top = 13, align = "start" }, {
|
|
||||||
made({ width = BAR_W + 4, height = stack_h, shrink = 0 }, function(q, bx, by)
|
|
||||||
drill_bars(g, q, bx, by + math.floor(stack_h / 2), queue, row_key)
|
|
||||||
end),
|
|
||||||
space(0, 1),
|
|
||||||
fg.label(p, (more > 0 and ("+" .. more .. " ") or "") .. label, style, 16, { fit = true, shrink = 1 }),
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
fg.build(flex.solve(tree, x, y, dw), p)
|
local tree = col({ width = w, height = SLOT_H, padding = { 0, 10 }, justify = "center", align = "center", gap = 2 }, {
|
||||||
|
fit(b, title, { font = "sans_semi", px = 13, color = ok and "cyan" or "muted", tracking = 0.06, upper = true }),
|
||||||
|
fit(b, sub, { font = "sans", px = 12, color = sub_color or "muted" }),
|
||||||
|
})
|
||||||
|
fg.build(flex.solve(tree, 0, 0), b)
|
||||||
|
if ok then
|
||||||
|
local params = { tool = grp.tool.id, target = well.id }
|
||||||
|
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)"))
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Table columns (468 wide at 1280x800; wider tables spread the columns): the supply
|
-- A drill's slot (SLOT_H tall; slides in when it was just deployed): its short name and
|
||||||
-- column up to the web's 200 px, the drill column takes what the row's buttons leave.
|
-- time left over its timer bar and what it gets; done (ready, or its end passed on our
|
||||||
local function table_cols(inner)
|
-- clock): gold, "+X ready", Claim (scope: claim-well or claim-wide) and, once unlocked,
|
||||||
local c2 = math.max(118, math.floor(inner * 0.25))
|
-- Redeploy. n: its place among the group's ready slots (the first one's buttons have
|
||||||
return { c2, math.max(92, math.min(212, math.floor(inner * 0.2))), "1fr", "auto" }
|
-- the plain action ids). wait: the group's next drill to finish; where its Claim and
|
||||||
|
-- Redeploy will show is registered as "wait:claim?..." (like H.claim_wait's waiting
|
||||||
|
-- button; not focusable), for the test driver.
|
||||||
|
local function drill_slot(g, st, d, scope, can_redeploy, n, wait, H, enters)
|
||||||
|
local tool = find(st.tools, d.toolId)
|
||||||
|
local due = d.ready or (d["end"] or math.huge) <= H.now(g)
|
||||||
|
local reach = reach_short(st, d.reach)
|
||||||
|
return made({ height = SLOT_H, shrink = 0 }, function(p, x, y, w)
|
||||||
|
local slot = ui.rect(p, x, y, w, SLOT_H, "none")
|
||||||
|
local body = ui.rect(slot, 0, 0, w, SLOT_H, due and "ready" or "cyan", due and 0.08 or 0.04)
|
||||||
|
ui.outline(body, 0, 0, w, SLOT_H, due and "ready" or "line_hi", due and 0.6 or nil)
|
||||||
|
local iw = w - 18
|
||||||
|
local right
|
||||||
|
local gets = d.here or d.take or d.amount or 0
|
||||||
|
if due then
|
||||||
|
-- (one that gets nothing is claimed all the same: it frees the drill)
|
||||||
|
right = fg.label(body, gets > 0 and ("+" .. ui.money(gets) .. " ready") or "Nothing left",
|
||||||
|
{ font = "sans_semi", px = 12, color = gets > 0 and "ready" or "muted", tracking = 0.08, upper = true }, 16, { shrink = 0 })
|
||||||
|
else
|
||||||
|
right = fg.label(body, ui.dur(d["end"] - H.now(g)), M12, 16, { shrink = 0 })
|
||||||
|
right.done = function(t)
|
||||||
|
H.tick_text(g, t, d["end"], ui.dur, "0:00", "left:" .. d.toolId)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- One row of the wells table (a well, or the multi-well group): a grid row 52 tall, its
|
|
||||||
-- row node (hover, selection, entrance) in a slot. enters: row entrances to run once the
|
|
||||||
-- page is built (they veil the row's content).
|
|
||||||
local function well_row(g, c, cols, max, moon, well, H, enters)
|
|
||||||
local st, v = g.state, g.view
|
|
||||||
local group = well.wide_group or well
|
|
||||||
local key = well.id or "wide"
|
|
||||||
local function made_row(parent, x, y, w, h)
|
|
||||||
local slot = ui.rect(parent, x, y, w, h, "none")
|
|
||||||
local r = ui.rect(slot, 0, 0, w, h, "cyan", 0)
|
|
||||||
ui.rect(r, 0, h - 1, w, 1, "line")
|
|
||||||
if not well.wide_group then
|
|
||||||
-- Row click selects the well (bound first: the buttons on it take precedence).
|
|
||||||
row_c.bind(g, r, w, h, { selected = well.id == v.well, id = "well:" .. well.id, label = well.resource,
|
|
||||||
on_click = function() H.navigate(g, { section = "moons", moon = moon.id, well = well.id }) end })
|
|
||||||
g.well_nodes[well.id] = r
|
|
||||||
end
|
end
|
||||||
-- A drill was just deployed here: the row slides in (fx-new).
|
local second
|
||||||
|
if due then
|
||||||
|
local suffix = n > 1 and ("#" .. n) or ""
|
||||||
|
local kids = {
|
||||||
|
fg.button(body, "Claim", { primary = true, small = true, h = SLOT_BTN_H }, function(b, bn)
|
||||||
|
H.action(g, b, bn, "claim", scope, nil, nil, n > 1 and (act_id("claim", scope) .. suffix) or nil)
|
||||||
|
tooltip.add(b, "Claim every ready drill here")
|
||||||
|
end, { grow = 1, height = SLOT_BTN_H }),
|
||||||
|
}
|
||||||
|
if can_redeploy then
|
||||||
|
local rp = { redeploy = "1" }
|
||||||
|
for k, v in pairs(scope) do
|
||||||
|
rp[k] = v
|
||||||
|
end
|
||||||
|
kids[2] = space(6)
|
||||||
|
kids[3] = fg.button(body, "Redeploy", { small = true, h = SLOT_BTN_H }, function(b, bn)
|
||||||
|
H.action(g, b, bn, "claim", rp, nil, nil, n > 1 and (act_id("claim", rp) .. suffix) or nil)
|
||||||
|
tooltip.add(b, "Claim, then send the drills out again")
|
||||||
|
end, { height = SLOT_BTN_H })
|
||||||
|
end
|
||||||
|
second = row({ height = SLOT_BTN_H, align = "center" }, kids)
|
||||||
|
else
|
||||||
|
local color = d.state == "short" and "ready" or "cyan"
|
||||||
|
local gets
|
||||||
|
if d.state == "none" then
|
||||||
|
gets = fit(body, "Nothing left by its turn", { font = "sans", px = 11, color = "ready" })
|
||||||
|
else
|
||||||
|
local here = ui.money(d.here or d.take or 0)
|
||||||
|
gets = fit(body, "Gets " .. here .. (d.state == "short" and (" of " .. ui.money(d.amount)) or "")
|
||||||
|
.. ((d.reach or 1) > 1 and (d.gets and d.gets ~= d.here) and (" · " .. ui.money(d.gets) .. " in all") or ""),
|
||||||
|
{ font = "sans", px = 11, color = d.state == "short" and "ready" or "muted" })
|
||||||
|
end
|
||||||
|
second = col({ height = SLOT_BTN_H, gap = 4, padding_top = 3 }, {
|
||||||
|
made({ height = 5, shrink = 0 }, function(q, bx, by, bw)
|
||||||
|
bar.timer(g, q, bx, by, bw, 5, { start = d.start, ends = d["end"], ready = false, color = color,
|
||||||
|
none = d.state == "none", key = "slot:" .. d.toolId })
|
||||||
|
end),
|
||||||
|
gets,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
local tree = col({ width = w, height = SLOT_H, padding = { 7, 9, 7, 9 }, gap = 4 }, {
|
||||||
|
row({ height = 16, align = "center" }, {
|
||||||
|
fit(body, M.short_name(tool), NAME13),
|
||||||
|
reach and space(5) or nil, reach and fit(body, "· " .. reach, S12) or nil,
|
||||||
|
fg.fill(), space(6), right,
|
||||||
|
}),
|
||||||
|
second,
|
||||||
|
})
|
||||||
|
fg.build(flex.solve(tree, 0, 0), body)
|
||||||
|
if wait and not due then
|
||||||
|
local function spot(params, style)
|
||||||
|
return made(style, function(q, bx, by, bw, bh)
|
||||||
|
test.register("wait:" .. act_id("claim", params):sub(5), ui.rect(q, bx, by, bw, bh, "none"), "wait", nil, nil, true)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
local kids = { spot(scope, { grow = 1, height = SLOT_BTN_H }) }
|
||||||
|
if can_redeploy then
|
||||||
|
local rp = { redeploy = "1" }
|
||||||
|
for k, v in pairs(scope) do
|
||||||
|
rp[k] = v
|
||||||
|
end
|
||||||
|
kids[2] = space(6)
|
||||||
|
kids[3] = spot(rp, { width = fg.button_w(body, "Redeploy", { small = true, h = SLOT_BTN_H }), height = SLOT_BTN_H, shrink = 0 })
|
||||||
|
end
|
||||||
|
fg.build(flex.solve(row({ width = w - 18, height = SLOT_BTN_H }, kids), 9, 27), body)
|
||||||
|
end
|
||||||
|
if d.fate and d.fate ~= "" then
|
||||||
|
tooltip.add(ui.rect(body, 0, 0, iw, 23, "none"), d.fate)
|
||||||
|
end
|
||||||
enters[#enters + 1] = function()
|
enters[#enters + 1] = function()
|
||||||
row_c.enter(g, "well:" .. key, r, w, h, { base = gui.get_color(r) })
|
row_c.enter(g, "drill:" .. d.toolId, body, w, SLOT_H, { base = gui.get_color(body) })
|
||||||
end
|
end
|
||||||
return r
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Drill queue: its own drills, and multi-well ones that reach it, oldest first.
|
-- The slot of the idle drills that last mined the well (well.redrill): how many, and
|
||||||
local drills = group.drills or {}
|
-- Redrill (sends them out again) once unlocked and the well has supply.
|
||||||
local queue = {}
|
local function idle_slot(g, st, well, H)
|
||||||
for _, d in ipairs(drills) do
|
local n = well.redrill or 0
|
||||||
queue[#queue + 1] = d
|
local names = {}
|
||||||
end
|
for _, t in ipairs(st.tools or {}) do
|
||||||
if not well.wide_group and moon.wide then
|
if #names < 2 and not t.stakeId and predict.memo.last_well[t.id] == well.id then
|
||||||
local reach = {}
|
names[#names + 1] = M.short_name(t)
|
||||||
for _, id in ipairs(well.wide or {}) do
|
|
||||||
reach[id] = true
|
|
||||||
end
|
|
||||||
for _, d in ipairs(moon.wide.drills or {}) do
|
|
||||||
if reach[d.id] then
|
|
||||||
queue[#queue + 1] = d
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.sort(queue, function(a, b) return (a.start or 0) < (b.start or 0) end)
|
local who = #names > 0 and (table.concat(names, ", ") .. (n > #names and (" +" .. (n - #names)) or "")) or ui.plural(n, "drill")
|
||||||
end
|
local can = st.unlocked.redrill and well.supply > 0
|
||||||
local fates = {}
|
return made({ height = SLOT_H, shrink = 0 }, function(p, x, y, w)
|
||||||
for _, d in ipairs(queue) do
|
local body = ui.rect(p, x, y, w, SLOT_H, "cyan", 0.02)
|
||||||
fates[#fates + 1] = d.fate
|
ui.outline(body, 0, 0, w, SLOT_H, "line_hi")
|
||||||
end
|
local tree = col({ width = w, height = SLOT_H, padding = { 7, 9, 7, 9 }, gap = 4 }, {
|
||||||
local label = (group.label or "") ~= "" and group.label or drill_label(st, drills) -- (the server's DrillLabel)
|
row({ height = 16, align = "center" }, {
|
||||||
|
fit(body, who, { font = "sans_semi", px = 13, color = "text_2" }), fg.fill(), space(6),
|
||||||
-- Name and supply cells.
|
fg.label(body, "Idle", { font = "sans_semi", px = 12, color = "muted", tracking = 0.08, upper = true }, 16, { shrink = 0 }),
|
||||||
local ML = { font = "mono", px = 12, color = "muted" }
|
}),
|
||||||
local name_cell, supply_cell
|
can and fg.button(body, "Redrill" .. (n > 1 and (" " .. n) or ""), { small = true, variant = "redrill", h = SLOT_BTN_H }, function(b, bn)
|
||||||
if well.wide_group then
|
H.action(g, b, bn, "redrill", { ["redrill-well"] = well.id })
|
||||||
name_cell = row({ padding_left = 26, align = "center" }, { fg.label(c, "Multi-well", "cap", 16) })
|
tooltip.add(b, "Send the drills that last mined this well out again")
|
||||||
-- "X left on the moon", wrapping onto a second line when the column is narrow.
|
end, { height = SLOT_BTN_H }) or row({ height = SLOT_BTN_H, align = "center" }, {
|
||||||
supply_cell = col({ padding_right = 8, justify = "center" }, {
|
fit(body, well.supply > 0 and "Last mined here" or "Last mined here · the well is empty", S11),
|
||||||
row({ wrap = true, column_gap = 5, row_gap = 1 }, {
|
|
||||||
fg.label(c, ui.money(group.cap), ML, 16),
|
|
||||||
fg.label(c, "left on the moon", { font = "sans", px = 12, color = "muted" }, 16, { fit = true, shrink = 1 }),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
else
|
fg.build(flex.solve(tree, 0, 0), body)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A well's line under its supply bar: the over-drilled lead in amber, else what the
|
||||||
|
-- drills leave ("45 free after drills"), else nothing (the line is kept).
|
||||||
|
local function well_note(c, well)
|
||||||
|
if well.supply <= 0 then
|
||||||
|
return fit(c, "Tapped out", { font = "sans_semi", px = 12, color = "muted", tracking = 0.1, upper = true })
|
||||||
|
elseif (well.statusText or "") ~= "" then
|
||||||
|
-- (its lead: "Over-drilled", "Fully committed"; the aside has the rest)
|
||||||
|
return fit(c, well.statusText:match("^(.-)%s+—") or well.statusText, { font = "sans", px = 12, color = "ready" })
|
||||||
|
elseif (well.demand or 0) > 0 then
|
||||||
|
return fit(c, ui.money(well.remain or 0) .. " free after drills", { font = "sans", px = 12, color = well.full and "ready" or "muted" })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The drill of a list that finishes next (not done yet), or nil.
|
||||||
|
local function next_drill(g, drills)
|
||||||
|
local now, pick = g.h.now(g), nil
|
||||||
|
for _, d in ipairs(drills or {}) do
|
||||||
|
if not d.ready and d["end"] and d["end"] > now and (not pick or d["end"] < pick["end"]) then
|
||||||
|
pick = d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return pick
|
||||||
|
end
|
||||||
|
|
||||||
|
-- One well's card (a grid cell; its node selects the well when clicked).
|
||||||
|
local function well_card(g, c, st, moon, well, max, groups, H, enters)
|
||||||
local dry = well.supply <= 0
|
local dry = well.supply <= 0
|
||||||
name_cell = row({ padding_left = 26, align = "center" }, {
|
local selected = well.id == g.view.well
|
||||||
made({ position = "absolute", left = 10, top = 22, width = 8, height = 8 }, function(q, x, y)
|
local scope = { ["claim-well"] = well.id }
|
||||||
ui.rect(q, x, y, 8, 8, well.resource, dry and 0.4 or 1)
|
local slots = {}
|
||||||
end),
|
if not dry then
|
||||||
fg.label(c, well.resource, { font = "sans", px = 15, color = dry and "muted" or "text" }, 16),
|
slots[#slots + 1] = deploy_slot(g, st, well, groups, H)
|
||||||
})
|
end
|
||||||
|
local n, next_d = 0, next_drill(g, well.drills)
|
||||||
|
for _, d in ipairs(well.drills or {}) do
|
||||||
|
local due = d.ready or (d["end"] or math.huge) <= H.now(g)
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
if (well.redrill or 0) > 0 then
|
||||||
|
slots[#slots + 1] = idle_slot(g, st, well, H)
|
||||||
|
end
|
||||||
|
local sup = fit(c, ui.money(well.supply), { font = "mono", px = 13, color = dry and "muted" or "text" }, { shrink = 0 })
|
||||||
|
sup.done = function(node) H.tick_num(g, node, "supply:" .. well.id, well.supply) end
|
||||||
|
local kids = {
|
||||||
|
row({ height = 18, align = "center" }, {
|
||||||
|
made({ width = 8, height = 8, shrink = 0 }, function(q, x, y) ui.rect(q, x, y, 8, 8, well.resource, dry and 0.4 or 1) end),
|
||||||
|
space(8), fit(c, well.resource, { font = "sans_semi", px = 15, color = dry and "muted" or "text" }),
|
||||||
|
fg.fill(), space(6), sup,
|
||||||
|
}),
|
||||||
|
space(0, 7),
|
||||||
|
made({ height = 6, shrink = 0 }, function(q, x, y, w)
|
||||||
if dry then
|
if dry then
|
||||||
supply_cell = row({ align = "center" }, {
|
ui.rect(q, x, y, w, 6, "line")
|
||||||
fg.label(c, "Tapped out", { font = "sans_semi", px = 12, color = "muted", tracking = 0.1, upper = true }, 16),
|
|
||||||
})
|
|
||||||
else
|
else
|
||||||
local sup = fg.label(c, ui.money(well.supply), ML, 16)
|
M.supply_bar(q, x, y, w, well, max)
|
||||||
sup.done = function(n) H.tick_num(g, n, "supply:" .. well.id, well.supply) end
|
end
|
||||||
supply_cell = col({ padding = { 20, 14, 0, 0 } }, {
|
end),
|
||||||
made({ height = 6 }, function(q, x, y, w) M.supply_bar(q, x, y, w, well, max) end),
|
space(0, 4),
|
||||||
space(0, 2), sup,
|
fg.reserve({ height = 16 }, well_note(c, well)),
|
||||||
|
space(0, 8),
|
||||||
|
col({ gap = SLOT_GAP }, slots),
|
||||||
|
}
|
||||||
|
return col({ padding = CARD_PAD }, kids, { make = function(parent, x, y, w, h)
|
||||||
|
local slot = ui.rect(parent, x, y, w, h, "none")
|
||||||
|
local card = ui.rect(slot, 0, 0, w, h, "cyan", 0)
|
||||||
|
row_c.bind(g, card, w, h, { selected = selected, id = "well:" .. well.id, label = well.resource,
|
||||||
|
on_click = function() H.navigate(g, { section = "moons", moon = moon.id, well = well.id }) end })
|
||||||
|
ui.outline(card, 0, 0, w, h, selected and "cyan" or "line_hi", selected and 0.9 or nil)
|
||||||
|
g.well_nodes[well.id] = card
|
||||||
|
return card
|
||||||
|
end })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The multi-well drills (moon.wide: reach > 1, claimed together with claim-wide): a card
|
||||||
|
-- across the grid, its slots in the cards' columns.
|
||||||
|
local function wide_card(g, c, st, moon, inner, H, enters)
|
||||||
|
local wide = moon.wide
|
||||||
|
local scope = { ["claim-wide"] = moon.id }
|
||||||
|
local slots, n, next_d = {}, 0, next_drill(g, wide.drills)
|
||||||
|
for _, d in ipairs(wide.drills or {}) do
|
||||||
|
local due = d.ready or (d["end"] or math.huge) <= H.now(g)
|
||||||
|
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)
|
||||||
|
end
|
||||||
|
local sw = inner - CARD_PAD * 2
|
||||||
|
return col({ width = inner, padding = CARD_PAD }, {
|
||||||
|
row({ height = 18, align = "center" }, {
|
||||||
|
fg.label(c, "Multi-well", { font = "sans_semi", px = 15, color = "text" }, 18), space(10),
|
||||||
|
fit(c, ui.money(wide.cap or 0) .. " left on the moon", S12), fg.fill(),
|
||||||
|
}),
|
||||||
|
space(0, 10),
|
||||||
|
flex.grid(sw, CARD_MIN - CARD_PAD * 2, SLOT_GAP, slots),
|
||||||
|
}, { make = function(parent, x, y, w, h)
|
||||||
|
local card = ui.rect(parent, x, y, w, h, "cyan", 0)
|
||||||
|
ui.outline(card, 0, 0, w, h, "line_hi")
|
||||||
|
return card
|
||||||
|
end })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The tray of idle drills: a tile per group (picture, short name, ×count, what a cycle
|
||||||
|
-- gets); a tile toggles the pick the deploy slots send.
|
||||||
|
local function tray(g, c, st, groups, inner, H)
|
||||||
|
local head = row({ height = 16, align = "center" }, {
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
if #groups == 0 then
|
||||||
|
return col({ width = inner }, {
|
||||||
|
head, space(0, 8),
|
||||||
|
row({ height = 16, align = "center" }, {
|
||||||
|
fg.label(c, "No idle drills.", "small", 16),
|
||||||
|
st.unlocked.store and space(5) or nil,
|
||||||
|
st.unlocked.store and made({ width = 160, height = 16, shrink = 0 }, function(p, x, y)
|
||||||
|
H.text_link(g, p, "Buy one in the store", { font = "sans", px = 13 }, x, y + 8, { section = "store", moon = g.view.moon })
|
||||||
|
end) or nil,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
local tiles = {}
|
||||||
|
for _, grp in ipairs(groups) do
|
||||||
|
local t = grp.tool
|
||||||
|
local picked = grp.key == g.deploy_pick
|
||||||
|
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 img = ui.rect(b, 6, 6, TILE_H - 12, TILE_H - 12, "bg")
|
||||||
|
if t.image then
|
||||||
|
images.set(ui.rect(img, 0, 0, TILE_H - 12, TILE_H - 12, "none"), t.image)
|
||||||
|
end
|
||||||
|
local reach = reach_short(st, t.reach)
|
||||||
|
local tree = col({ width = w, height = TILE_H, padding = { 8, 8, 8, TILE_H }, gap = 4 }, {
|
||||||
|
row({ height = 16, align = "center" }, {
|
||||||
|
fit(b, M.short_name(t), NAME13), fg.fill(), space(4),
|
||||||
|
fg.label(b, "×" .. grp.count, { font = "mono", px = 12, color = picked and "cyan" or "text_2" }, 16, { shrink = 0 }),
|
||||||
|
}),
|
||||||
|
fit(b, "+" .. ui.money(t.yield or 0) .. " / " .. ui.cycle(t.cycleSecs) .. (reach and (" · " .. reach) or ""), { font = "mono", px = 11, color = "muted" }),
|
||||||
|
})
|
||||||
|
fg.build(flex.solve(tree, 0, 0), b)
|
||||||
|
if picked then
|
||||||
|
ui.outline(b, 0, 0, w, TILE_H, "cyan", 0.9)
|
||||||
|
end
|
||||||
|
tooltip.add(b, (t.name or "Drill") .. (picked and " · picked (click to unpick)" or " · pick it for + Deploy"))
|
||||||
|
button.bind(g, b, bn, { id = "pick:" .. grp.key, label = M.short_name(t), on_click = function()
|
||||||
|
g.deploy_pick = (not picked) and grp.key or nil
|
||||||
|
g.dirty = true
|
||||||
|
end })
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
return col({ width = inner }, { head, space(0, 8), flex.grid(inner, TILE_MIN, 8, tiles) })
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Status (right): Claim (ready), Claim countdown (mining), Redrill (idle tools that
|
|
||||||
-- mined it), then Manage (View when tapped out), like the web's row actions.
|
|
||||||
-- (the amount moves on as more of its drills finish on our clock: core/ready)
|
|
||||||
local scope = well.wide_group and { ["claim-wide"] = moon.id } or { ["claim-well"] = well.id }
|
|
||||||
local amount = ready.amount(g, group, well.wide_group and group.cap or well.supply)
|
|
||||||
local status = {}
|
|
||||||
if group.ready > 0 then
|
|
||||||
status[#status + 1] = fg.button(c, "Claim " .. ui.money(amount), { primary = true, small = true }, function(b, bn)
|
|
||||||
H.action(g, b, bn, "claim", scope)
|
|
||||||
end)
|
|
||||||
elseif group.nextEnd then
|
|
||||||
local wopts = { small = true, right = true, key = "wait:" .. key, params = scope, moon = moon.id,
|
|
||||||
done = amount > 0 and ("Claim " .. ui.money(amount)) or nil }
|
|
||||||
local bw = H.claim_wait_w(g, c, group.nextEnd, wopts)
|
|
||||||
status[#status + 1] = made({ width = bw, height = S.button_sm_h, shrink = 0 }, function(q, x, y, w)
|
|
||||||
H.claim_wait(g, q, x + w, y, group.nextEnd, wopts)
|
|
||||||
end)
|
|
||||||
elseif not well.wide_group and (well.redrill or 0) > 0 and well.supply > 0 and st.unlocked.redrill then
|
|
||||||
status[#status + 1] = fg.button(c, "Redrill", { small = true, variant = "redrill" }, function(b, bn)
|
|
||||||
H.action(g, b, bn, "redrill", { ["redrill-well"] = well.id })
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
if not well.wide_group then
|
|
||||||
local to = { section = "moons", moon = moon.id, well = well.id }
|
|
||||||
status[#status + 1] = fg.button(c, well.supply <= 0 and "View" or "Manage", { small = true }, function(b, bn)
|
|
||||||
button.bind(g, b, bn, { id = "manage:" .. well.id, on_click = function() H.navigate(g, to) end })
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return box({ columns = cols, rows = { S.well_row_h }, padding_right = 8 }, {
|
|
||||||
name_cell, supply_cell,
|
|
||||||
drill_cell(g, queue, fates, label, key, #status > 0 and 14 or 8),
|
|
||||||
row({ gap = 6, align = "center" }, status),
|
|
||||||
}, { make = made_row })
|
|
||||||
end
|
|
||||||
|
|
||||||
local function rating_chip(c, r)
|
local function rating_chip(c, r)
|
||||||
-- The web's rating chip ("★3" in the tier's color; its tooltip the label and percentile).
|
-- The web's rating chip ("★3" in the tier's color; its tooltip the label and percentile).
|
||||||
@@ -412,8 +657,8 @@ local function rating_chip(c, r)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- The moon: heading (id, name, class and rating, description; Claim at the right), the
|
-- The moon: heading (id, name, class and rating, description; Claim at the right), the
|
||||||
-- wells table (min_h tall at least; the moon's sale is in the aside). Returns its
|
-- well cards, the multi-well card, the tray of idle drills, and selling the moon (once
|
||||||
-- bottom.
|
-- the store is open). min_h tall at least. Returns its bottom.
|
||||||
local function moon_view(g, c, px, py, inner, min_h, H)
|
local function moon_view(g, c, px, py, inner, min_h, H)
|
||||||
local st = g.state
|
local st = g.state
|
||||||
local moon = st.moon
|
local moon = st.moon
|
||||||
@@ -456,32 +701,43 @@ local function moon_view(g, c, px, py, inner, min_h, H)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Wells table: the header, an edge line, a row per well and the multi-well group.
|
-- The cards: a well each (as many columns as CARD_MIN allows), the multi-well card
|
||||||
|
-- across them, then the idle drills.
|
||||||
local max = M.supply_max(st, moon)
|
local max = M.supply_max(st, moon)
|
||||||
local cols = table_cols(inner)
|
local groups = M.idle_groups(st)
|
||||||
|
local pick_ok = false
|
||||||
|
for _, grp in ipairs(groups) do
|
||||||
|
pick_ok = pick_ok or grp.key == g.deploy_pick
|
||||||
|
end
|
||||||
|
if not pick_ok then
|
||||||
|
g.deploy_pick = nil -- (used up)
|
||||||
|
end
|
||||||
local enters = {}
|
local enters = {}
|
||||||
local tbl = {
|
local cards = {}
|
||||||
box({ columns = cols, height = 16, align = "start" }, {
|
|
||||||
fg.label(c, "Well", "cap", 12), fg.label(c, "Supply left", "cap", 12), fg.label(c, "Drill", "cap", 12),
|
|
||||||
fg.label(c, "Status", "cap", 12, { justify_self = "end" }),
|
|
||||||
}),
|
|
||||||
made({ position = "absolute", top = 16, left = 0, right = 0, height = 1 }, function(p, x, y, w)
|
|
||||||
ui.rect(p, x, y, w, 1, "edge")
|
|
||||||
end),
|
|
||||||
}
|
|
||||||
for _, well in ipairs(moon.wells) do
|
for _, well in ipairs(moon.wells) do
|
||||||
tbl[#tbl + 1] = well_row(g, c, cols, max, moon, well, H, enters)
|
cards[#cards + 1] = well_card(g, c, st, moon, well, max, groups, H, enters)
|
||||||
end
|
end
|
||||||
if moon.wide then
|
local kids = {
|
||||||
tbl[#tbl + 1] = well_row(g, c, cols, max, moon, { wide_group = moon.wide }, H, enters)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- (selling the moon is in the aside, under the well: aside.lua)
|
|
||||||
local page = col({ width = inner, min_height = min_h }, {
|
|
||||||
col({ min_height = 80, align = "start" }, head),
|
col({ min_height = 80, align = "start" }, head),
|
||||||
space(0, 14),
|
space(0, 14),
|
||||||
col({}, tbl),
|
flex.grid(inner, CARD_MIN, CARD_GAP, cards),
|
||||||
})
|
}
|
||||||
|
if moon.wide and #(moon.wide.drills or {}) > 0 then
|
||||||
|
kids[#kids + 1] = space(0, CARD_GAP)
|
||||||
|
kids[#kids + 1] = wide_card(g, c, st, moon, inner, H, enters)
|
||||||
|
end
|
||||||
|
kids[#kids + 1] = space(0, 20)
|
||||||
|
kids[#kids + 1] = tray(g, c, st, groups, inner, H)
|
||||||
|
|
||||||
|
-- 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)
|
||||||
fg.build(flex.solve(page, px, py), c)
|
fg.build(flex.solve(page, px, py), c)
|
||||||
for _, f in ipairs(enters) do
|
for _, f in ipairs(enters) do
|
||||||
f()
|
f()
|
||||||
@@ -489,6 +745,7 @@ local function moon_view(g, c, px, py, inner, min_h, H)
|
|||||||
return page.y + page.h
|
return page.y + page.h
|
||||||
end
|
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)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
-- The Store section (state.store): tool cards and attachment cards with their images,
|
-- The Store section (state.store): tool cards with their images,
|
||||||
-- prices, what they'd earn and how many you own. Buying a tool opens the buy modal
|
-- prices, what they'd earn and how many you own. Buying a tool opens the buy modal
|
||||||
-- (the response's buyModal: up to 3 wells to deploy it on).
|
-- (the response's buyModal: up to 3 wells to deploy it on).
|
||||||
|
|
||||||
@@ -185,26 +185,6 @@ local function tool_card(self, H, st, c, t)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function gear_card(self, H, st, c, a)
|
|
||||||
c:image(a.image)
|
|
||||||
c:title(a.name)
|
|
||||||
c:text(a.effectText or cards.effect(a.effect, "attachment"), { font = "mono", px = 14, color = "violet_hi" }, 22)
|
|
||||||
c:para(a.description, { font = "sans", px = 13, color = "text_2" })
|
|
||||||
c:gap(4)
|
|
||||||
local short = not a.canAfford
|
|
||||||
c:footer(30 + S.button_sm_h + 18, function(f, bw) -- (the need line's place is kept: buy_footer)
|
|
||||||
M.buy_footer(f, bw, { { "Spare", a.spare }, { "Equipped", a.equipped } }, a.price, short,
|
|
||||||
(a.whyText or "") ~= "" and a.whyText or ("Need " .. theme.currency .. ui.money(a.price - st.credits) .. " more"),
|
|
||||||
function(b, nodes)
|
|
||||||
if short then
|
|
||||||
tooltip.add(b, a.whyText)
|
|
||||||
else
|
|
||||||
H.action(self, b, nodes, "buy-attachment", { item = a.name })
|
|
||||||
end
|
|
||||||
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 sto = st.store
|
local sto = st.store
|
||||||
@@ -217,11 +197,10 @@ function M.build(self, root, px, py, w, h, H)
|
|||||||
local function cap(label, before)
|
local function cap(label, before)
|
||||||
return fg.col({}, { fg.space(0, before), fg.label(c, label, "cap", 12), fg.space(0, 8) })
|
return fg.col({}, { fg.space(0, before), fg.label(c, label, "cap", 12), fg.space(0, 8) })
|
||||||
end
|
end
|
||||||
local attachments = #(sto.attachments or {}) > 0
|
|
||||||
y = fg.flow(c, x, y, inner, {
|
y = fg.flow(c, x, y, inner, {
|
||||||
fg.col({}, {
|
fg.col({}, {
|
||||||
fg.label(c, "Store", "h1", 40), fg.space(0, 4),
|
fg.label(c, "Store", "h1", 40), fg.space(0, 4),
|
||||||
fg.text_para(c, "Most tools mine one well at a time; the biggest mine several wells of a moon, or the whole moon, each cycle. Equip attachments in Equipment.",
|
fg.text_para(c, "Most tools mine one well at a time; the biggest mine several wells of a moon, or the whole moon, each cycle. Upgrade them in Equipment.",
|
||||||
{ font = "sans", px = 15, color = "muted" }, { max_width = math.min(inner, S.text_max) }),
|
{ font = "sans", px = 15, color = "muted" }, { max_width = math.min(inner, S.text_max) }),
|
||||||
fg.space(0, 20),
|
fg.space(0, 20),
|
||||||
}),
|
}),
|
||||||
@@ -231,12 +210,6 @@ function M.build(self, root, px, py, w, h, H)
|
|||||||
tool_card(self, H, st, cd, t)
|
tool_card(self, H, st, cd, t)
|
||||||
end, { key = function(t) return "store:tool:" .. tostring(t.storeItemId) end, sig = shared })
|
end, { key = function(t) return "store:tool:" .. tostring(t.storeItemId) end, sig = shared })
|
||||||
end,
|
end,
|
||||||
attachments and cap("Attachments", 8) or nil,
|
|
||||||
attachments and function(cy)
|
|
||||||
return card.grid(self, c, x, cy, inner, sto.attachments, function(cd, a)
|
|
||||||
gear_card(self, H, st, cd, a)
|
|
||||||
end, { key = function(a) return "store:gear:" .. tostring(a.name) end, sig = shared })
|
|
||||||
end or nil,
|
|
||||||
})
|
})
|
||||||
area:finish(y + py)
|
area:finish(y + py)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Usage: scripts/fixture.py <api base> [--plain]
|
|||||||
Prints one JSON line: {"player", "moon", "well", "wells", "tool", "drill_mining", ...}
|
Prints one JSON line: {"player", "moon", "well", "wells", "tool", "drill_mining", ...}
|
||||||
that scenarios (scripts/drive.py) use for views. The player: every section unlocked,
|
that scenarios (scripts/drive.py) use for views. The player: every section unlocked,
|
||||||
credits, resources and refined items granted, two moons, several tools (some drilling,
|
credits, resources and refined items granted, two moons, several tools (some drilling,
|
||||||
one ready, one idle with an attachment), a refinery batch running, an advisor hired.
|
one ready, one idle with upgraded tracks), a refinery batch running, an advisor hired.
|
||||||
--plain: just a new player (nothing granted), for the first-moon views.
|
--plain: just a new player (nothing granted), for the first-moon views.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
@@ -66,8 +66,8 @@ admin("credits", amount=500000, op="grant")
|
|||||||
for r in ["Sollux", "Shadowstone", "Azurium", "Novafor", "Nebulance",
|
for r in ["Sollux", "Shadowstone", "Azurium", "Novafor", "Nebulance",
|
||||||
"Dusk Alloy", "Cryo Cell", "Plasma Weave"]:
|
"Dusk Alloy", "Cryo Cell", "Plasma Weave"]:
|
||||||
admin("resources", resource=r, amount=400)
|
admin("resources", resource=r, amount=400)
|
||||||
for item, tier in [(1, 0), (1, 0), (2, 1), (3, 0), (5, 2)]:
|
for item, yl, cl in [(1, 0, 0), (1, 0, 0), (2, 0, 1), (3, 0, 0), (5, 2, 1)]:
|
||||||
admin("tool", item=item, tier=tier)
|
admin("tool", item=item, **{"yield": yl, "cycle": cl})
|
||||||
|
|
||||||
act("buy-moon")
|
act("buy-moon")
|
||||||
d = act("buy-moon")
|
d = act("buy-moon")
|
||||||
@@ -89,11 +89,11 @@ act("stake", tool=long_tool, target=wells[0]["id"])
|
|||||||
act("stake", tool=by_item[2][0]["id"], target=wells[1 % len(wells)]["id"])
|
act("stake", tool=by_item[2][0]["id"], target=wells[1 % len(wells)]["id"])
|
||||||
act("stake", tool=by_item[3][0]["id"], target=wells[-1]["id"])
|
act("stake", tool=by_item[3][0]["id"], target=wells[-1]["id"])
|
||||||
|
|
||||||
act("buy-attachment", item="Turbo Core")
|
# The idle PD-1 a level up on each track (its yield track twice).
|
||||||
act("buy-attachment", item="Diamond Bit")
|
|
||||||
act("buy-attachment", item="Diamond Bit")
|
|
||||||
idle = by_item[1][1]["id"]
|
idle = by_item[1][1]["id"]
|
||||||
act("equip", item="Turbo Core", to=idle)
|
act("upgrade", tool=idle, track="yield")
|
||||||
|
act("upgrade", tool=idle, track="yield")
|
||||||
|
act("upgrade", tool=idle, track="cycle")
|
||||||
|
|
||||||
act("buy-slot")
|
act("buy-slot")
|
||||||
act("refine", recipe="Dusk Alloy", qty=5)
|
act("refine", recipe="Dusk Alloy", qty=5)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""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, tiers) without the Store's parts (upgrade,
|
Equipment, a tool card opens its panel (stats, the tracks' ladders) without the Store's
|
||||||
slots, spares, Sell drill); after the first claim and sale reveal the Store, the same panel
|
parts (the upgrade buttons, Sell drill); after the first claim and sale reveal the Store,
|
||||||
has them. Shots "before" and "after", each linted."""
|
the same panel has them. Shots "before" and "after", each linted."""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -41,10 +41,10 @@ def run(game, args):
|
|||||||
game.shot("before")
|
game.shot("before")
|
||||||
game.lint("before")
|
game.lint("before")
|
||||||
texts = labels(game)
|
texts = labels(game)
|
||||||
game.expect("tiers" in texts, "tier ladder shown")
|
game.expect("yield per cycle" in texts and "level" in texts, "track ladders shown")
|
||||||
for t in ("slots", "spare attachments", "sell drill"):
|
game.expect("sell drill" not in texts, "sell drill hidden before the Store")
|
||||||
game.expect(t not in texts, "%s hidden before the Store" % t)
|
ups = [e["id"] for e in game.elements() if e["id"].startswith("act:upgrade?")]
|
||||||
game.expect(not any(t.startswith("upgrade to") for t in texts), "no upgrade before the Store")
|
game.expect(not ups, "no upgrade before the Store: %s" % ups)
|
||||||
|
|
||||||
# First claim (Market), first sale (Store).
|
# First claim (Market), first sale (Store).
|
||||||
admin(game, "finish-stakes")
|
admin(game, "finish-stakes")
|
||||||
@@ -60,5 +60,8 @@ def run(game, args):
|
|||||||
game.shot("after")
|
game.shot("after")
|
||||||
game.lint("after")
|
game.lint("after")
|
||||||
texts = labels(game)
|
texts = labels(game)
|
||||||
for t in ("tiers", "slots", "spare attachments", "sell drill"):
|
for t in ("yield per cycle", "sell drill"):
|
||||||
game.expect(t in texts, "%s shown after the Store" % t)
|
game.expect(t in texts, "%s shown after the Store" % t)
|
||||||
|
ups = {e["id"] for e in game.elements() if e["id"].startswith("act:upgrade?")}
|
||||||
|
for kind in ("yield", "cycle"):
|
||||||
|
game.expect(any(("track=" + kind) in i for i in ups), "%s upgrade after the Store: %s" % (kind, ups))
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
scenario may change: MM_DB, default mm_c_lists, in the podman container MM_DB_CONTAINER,
|
scenario may change: MM_DB, default mm_c_lists, in the podman container MM_DB_CONTAINER,
|
||||||
default moonminers_db_1). Shots: a depleted moon row (and hovered), the mined-out
|
default moonminers_db_1). Shots: a depleted moon row (and hovered), the mined-out
|
||||||
collapse and the Past tab flash, the greyed Past list, a new moon row entering, a new
|
collapse and the Past tab flash, the greyed Past list, a new moon row entering, a new
|
||||||
drill entering (wells table and well panel), deploy cards short and spoken for."""
|
drill entering (well card and well panel), the deploy slot short and with no room."""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -98,9 +98,9 @@ def run(game, args):
|
|||||||
burst(game, "newmoon", [0.05, 0.3, 0.6, 1.2])
|
burst(game, "newmoon", [0.05, 0.3, 0.6, 1.2])
|
||||||
game.idle()
|
game.idle()
|
||||||
|
|
||||||
# Deploy cards: tools idle after claiming moon 1; a well with little left: a stake
|
# Deploying: tools idle after claiming moon 1; a well with little left: a deploy from
|
||||||
# from the well panel (the new drill enters in the table and the panel), then the
|
# its card's slot (the new drill enters in the card and the well panel), then the slot
|
||||||
# other cards come up short or spoken for.
|
# comes up short ("Only +N left here"), then "No room here" once nothing is left.
|
||||||
game.navigate("{section='moons', moon='%s'}" % fx["moon"])
|
game.navigate("{section='moons', moon='%s'}" % fx["moon"])
|
||||||
game.idle()
|
game.idle()
|
||||||
if game.eval("st.ready") > 0:
|
if game.eval("st.ready") > 0:
|
||||||
@@ -112,24 +112,17 @@ def run(game, args):
|
|||||||
game.navigate("{section='moons', moon='%s', well='%s'}" % (fx["moon"], well))
|
game.navigate("{section='moons', moon='%s', well='%s'}" % (fx["moon"], well))
|
||||||
game.idle()
|
game.idle()
|
||||||
game.shot("deploy")
|
game.shot("deploy")
|
||||||
# (ids from the state: the elements reply can be long)
|
slot = "act:stake?target=%s" % well
|
||||||
def card(i):
|
first = next((e["id"] for e in game.elements() if e["id"].startswith(slot)), None)
|
||||||
tool = game.eval("st.well.idle[%d] and st.well.idle[%d].toolId" % (i, i))
|
game.expect(first, "the well's deploy slot is there")
|
||||||
return tool and "act:stake?target=%s&tool=%s" % (well, tool)
|
|
||||||
first = card(1)
|
|
||||||
game.expect(first, "deploy cards are there")
|
|
||||||
game.scroll_to(first) # (the cards and the drills list under them in view)
|
|
||||||
if first:
|
if first:
|
||||||
|
game.scroll_to(first)
|
||||||
game.click(first)
|
game.click(first)
|
||||||
burst(game, "newdrill", [0.05, 0.3, 0.6, 1.2])
|
burst(game, "newdrill", [0.05, 0.3, 0.6, 1.2])
|
||||||
game.idle()
|
game.idle()
|
||||||
game.shot("deploy-short", settle=0.5)
|
game.shot("deploy-short", settle=0.5)
|
||||||
# Nothing left after the drills: spoken for (dimmed; less so under the mouse).
|
# Nothing left after the drills: no room.
|
||||||
psql("update resource_well set supply = 2 where id = '%s'" % well)
|
psql("update resource_well set supply = 2 where id = '%s'" % well)
|
||||||
game.navigate("{section='moons', moon='%s', well='%s'}" % (fx["moon"], well))
|
game.navigate("{section='moons', moon='%s', well='%s'}" % (fx["moon"], well))
|
||||||
game.idle()
|
game.idle()
|
||||||
game.shot("deploy-spoken", settle=0.5)
|
game.shot("deploy-spoken", settle=0.5)
|
||||||
n = game.eval("#st.well.idle")
|
|
||||||
if n:
|
|
||||||
game.hover(card(n))
|
|
||||||
game.shot("deploy-hover", settle=0.4)
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
"""The moon view and well panel in the web's look (needs --fixture and a local dev server
|
"""The moon view and well panel (needs --fixture and a local dev server whose database the
|
||||||
whose database the scenario may change: MM_DB, see lists.py): the wells table (stacked
|
scenario may change: MM_DB, see lists.py): the well cards (a card per well, the rating
|
||||||
drill bars, Manage on every row, the rating chip), drills that get nothing (amber dashed
|
chip), drills that get nothing (amber dashed bars on their slots; in the aside the NOTHING
|
||||||
bars, the NOTHING LEFT tag, a greyed picture), a tapped-out well's panel, and the moon
|
LEFT tag, a greyed picture), a tapped-out well, and the moon sale opened (its lines scroll
|
||||||
sale opened (its lines scroll with the moon). Args: an optional name prefix."""
|
with the moon). Args: an optional name prefix."""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -19,13 +19,13 @@ def run(game, args):
|
|||||||
game.idle()
|
game.idle()
|
||||||
game.shot(pre + "table", settle=0.8)
|
game.shot(pre + "table", settle=0.8)
|
||||||
ids = [e["id"] for e in game.elements()]
|
ids = [e["id"] for e in game.elements()]
|
||||||
wells = [w for w in fx["wells"] if ("manage:" + w) in ids]
|
wells = [w for w in fx["wells"] if ("well:" + w) in ids]
|
||||||
game.expect(len(wells) == len(fx["wells"]), "Manage on every well row (%d of %d)" % (len(wells), len(fx["wells"])))
|
game.expect(len(wells) == len(fx["wells"]), "a card for every well (%d of %d)" % (len(wells), len(fx["wells"])))
|
||||||
|
|
||||||
# Drills that get nothing: six more on a well, then the well holds almost nothing.
|
# Drills that get nothing: six more on a well, then the well holds almost nothing.
|
||||||
well = fx["wells"][1 % len(fx["wells"])]
|
well = fx["wells"][1 % len(fx["wells"])]
|
||||||
for _ in range(6):
|
for _ in range(6):
|
||||||
admin(game, "tool", item=2, tier=0)
|
admin(game, "tool", item=2, **{"yield": 0, "cycle": 0})
|
||||||
st = call(game, "GET", "/api/state")["state"]
|
st = call(game, "GET", "/api/state")["state"]
|
||||||
idle = [t["id"] for t in st["tools"] if t["storeItemId"] == 2 and not t.get("stakeId")][:6]
|
idle = [t["id"] for t in st["tools"] if t["storeItemId"] == 2 and not t.get("stakeId")][:6]
|
||||||
for tid in idle:
|
for tid in idle:
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"""Parity polish (needs --fixture). Phases (args, default all but ready):
|
"""Parity polish (needs --fixture). Phases (args, default all but ready):
|
||||||
bars: six more drills on a well: the wells table's bars with "+N" and the multi-well
|
bars: six more drills on a well: their slots on the well's card, the multi-well card,
|
||||||
diamond caps, the well panel in the web's order (deploy, drills, then the claims).
|
the well panel's drill lines (it only shows: the actions are on the cards).
|
||||||
ready: run with --config mm.fake_latency_ms=2500: drills finish while the refetch is
|
ready: run with --config mm.fake_latency_ms=2500: drills finish while the refetch is
|
||||||
held back; the header badge, Claim all, moon row badge and Claim buttons count up on
|
held back; the header badge, Claim all, moon row badge and Claim buttons count up on
|
||||||
our clock ("Claim N drills", "Claim <amount>").
|
our clock ("Claim N drills", "Claim <amount>").
|
||||||
motion: the header's reduced-motion toggle (run with --config mm.reduced_motion=0: forced,
|
motion: the header's reduced-motion toggle (run with --config mm.reduced_motion=0: forced,
|
||||||
so nothing is saved), the still ticker, a reduced reveal.
|
so nothing is saved), the still ticker, a reduced reveal.
|
||||||
links: "N idle <tool>" opens that copy, "Better" well link, market card hover, refinery
|
links: "N idle <tool>" opens that copy, market card hover, refinery recipes (the
|
||||||
recipes."""
|
"Better" well is plain text in the well panel now: its link shot is skipped)."""
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -47,7 +47,7 @@ def idle_tools(game, item):
|
|||||||
|
|
||||||
def stake_many(game, item, n, well):
|
def stake_many(game, item, n, well):
|
||||||
for _ in range(n):
|
for _ in range(n):
|
||||||
admin(game, "tool", item=item, tier=0)
|
admin(game, "tool", item=item, **{"yield": 0, "cycle": 0})
|
||||||
for tid in idle_tools(game, item)[:n]:
|
for tid in idle_tools(game, item)[:n]:
|
||||||
r = call(game, "POST", "/api/act/stake", {"tool": tid, "target": well})
|
r = call(game, "POST", "/api/act/stake", {"tool": tid, "target": well})
|
||||||
game.expect(r.get("ok"), "stake: %s" % r.get("error"))
|
game.expect(r.get("ok"), "stake: %s" % r.get("error"))
|
||||||
@@ -84,7 +84,7 @@ def ready(game):
|
|||||||
free = [w]
|
free = [w]
|
||||||
well = free[0]["id"]
|
well = free[0]["id"]
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
admin(game, "tool", item=3, tier=0)
|
admin(game, "tool", item=3, **{"yield": 0, "cycle": 0})
|
||||||
copies = idle_tools(game, 3)[:2]
|
copies = idle_tools(game, 3)[:2]
|
||||||
for tid in copies:
|
for tid in copies:
|
||||||
r = call(game, "POST", "/api/act/stake", {"tool": tid, "target": well})
|
r = call(game, "POST", "/api/act/stake", {"tool": tid, "target": well})
|
||||||
@@ -143,7 +143,7 @@ def links(game):
|
|||||||
fx = game.fixture
|
fx = game.fixture
|
||||||
# Two LunarLite L100 (10 s cycles), one mining: its panel links to the idle one.
|
# Two LunarLite L100 (10 s cycles), one mining: its panel links to the idle one.
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
admin(game, "tool", item=3, tier=0)
|
admin(game, "tool", item=3, **{"yield": 0, "cycle": 0})
|
||||||
copies = idle_tools(game, 3)
|
copies = idle_tools(game, 3)
|
||||||
r = call(game, "POST", "/api/act/stake", {"tool": copies[0], "target": fx["wells"][0]})
|
r = call(game, "POST", "/api/act/stake", {"tool": copies[0], "target": fx["wells"][0]})
|
||||||
game.expect(r.get("ok"), "stake L100: %s" % r.get("error"))
|
game.expect(r.get("ok"), "stake L100: %s" % r.get("error"))
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"""Sells ask in place, not in a dialog (needs --fixture; cards.ask_button): the drill
|
"""Sells ask in place, not in a dialog (needs --fixture; cards.ask_button): the drill
|
||||||
panel's spare attachment Sell turns into Cancel on its spot with Confirm just left of it and
|
panel's Sell (an idle copy) turns into Cancel on its spot with Confirm just left of it and
|
||||||
the server's question where the text was; no dialog opens; nothing else moves. It cancels
|
the server's question where the text was; no dialog opens; nothing else moves. It cancels
|
||||||
on Esc, on Cancel, on a click elsewhere and after ASK_IDLE seconds; Enter confirms (Confirm
|
on Esc, on Cancel, on a click elsewhere and after ASK_IDLE seconds; Enter confirms (Confirm
|
||||||
has the soft focus) and sells. Then the drill's own Sell (asks, cancelled) and the moon's
|
has the soft focus) and sells the drill. Then the moon's Sell moon (shot of its asking state, when the moon can be sold). Shots of each."""
|
||||||
Sell moon (shot of its asking state, when the moon can be sold). Shots of each."""
|
|
||||||
|
|
||||||
|
|
||||||
def rect_of(game, id_):
|
def rect_of(game, id_):
|
||||||
@@ -49,9 +48,9 @@ def covers(outer, inner):
|
|||||||
def run(game, args):
|
def run(game, args):
|
||||||
fx = game.fixture
|
fx = game.fixture
|
||||||
pre = (args[0] + "-") if args else ""
|
pre = (args[0] + "-") if args else ""
|
||||||
sell = "confirm:sell-attachment"
|
sell = "confirm:sell-tool"
|
||||||
game.idle()
|
game.idle()
|
||||||
game.navigate("{section='equipment', drill='%s'}" % fx["drill_mining"])
|
game.navigate("{section='equipment', drill='%s'}" % fx["tool_idle"])
|
||||||
game.idle()
|
game.idle()
|
||||||
game.scroll_to(sell)
|
game.scroll_to(sell)
|
||||||
before = rect_of(game, sell)
|
before = rect_of(game, sell)
|
||||||
@@ -95,16 +94,6 @@ def run(game, args):
|
|||||||
game.expect(game.log_has(r"mm: toast Sold"), "Enter sells (a toast)")
|
game.expect(game.log_has(r"mm: toast Sold"), "Enter sells (a toast)")
|
||||||
game.shot(pre + "sellask-sold", settle=0.4)
|
game.shot(pre + "sellask-sold", settle=0.4)
|
||||||
|
|
||||||
# The drill's own Sell: asks, a shot, cancelled.
|
|
||||||
game.navigate("{section='equipment', drill='%s'}" % fx["tool_idle"])
|
|
||||||
game.idle()
|
|
||||||
if rect_of(game, "confirm:sell-tool"):
|
|
||||||
ask(game, "confirm:sell-tool")
|
|
||||||
game.shot(pre + "sellask-tool", settle=0.3)
|
|
||||||
game.key("key_esc")
|
|
||||||
game.wait_for("g.ask == nil", 5)
|
|
||||||
game.idle()
|
|
||||||
|
|
||||||
# The moon's Sell moon (when it can be sold yet).
|
# The moon's Sell moon (when it can be sold yet).
|
||||||
game.navigate("{section='moons', moon='%s'}" % fx["moon2"])
|
game.navigate("{section='moons', moon='%s'}" % fx["moon2"])
|
||||||
game.idle()
|
game.idle()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Narrow windows (run with --size 1024x640; needs --fixture): the aside gets its own row
|
"""Narrow windows (run with --size 1024x640; needs --fixture): the aside gets its own row
|
||||||
under the moon list and the main panel and the page scrolls to it (a well, the drill
|
under the moon list and the main panel and the page scrolls to it (the drill panel; a
|
||||||
panel); sections without the aside use the whole width; the banner wraps; keys scroll
|
well's card stays in view); sections without the aside use the whole width; the banner wraps; keys scroll
|
||||||
the page. Args: an optional name prefix."""
|
the page. Args: an optional name prefix."""
|
||||||
|
|
||||||
|
|
||||||
@@ -18,6 +18,11 @@ def run(game, args):
|
|||||||
game.click("well:" + well)
|
game.click("well:" + well)
|
||||||
game.wait_for("view.well == '%s' and not busy" % well)
|
game.wait_for("view.well == '%s' and not busy" % well)
|
||||||
game.idle()
|
game.idle()
|
||||||
|
# (picking a well card keeps the cards in view: its actions are there)
|
||||||
|
game.expect((game.eval("g.scroll_pos.page or 0") or 0) < 1, "picking a well doesn't scroll the page")
|
||||||
|
game.shot(pre + "small-well-card", settle=0.8)
|
||||||
|
game.hover("tab:market")
|
||||||
|
game.key("key_end")
|
||||||
game.wait_for("(g.scroll_pos.page or 0) > 100", timeout=5)
|
game.wait_for("(g.scroll_pos.page or 0) > 100", timeout=5)
|
||||||
game.shot(pre + "small-well", settle=1.0)
|
game.shot(pre + "small-well", settle=1.0)
|
||||||
# (keys scroll the area under the mouse: the moon itself scrolls when it doesn't fit,
|
# (keys scroll the area under the mouse: the moon itself scrolls when it doesn't fit,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Server texts in the confirms (needs --fixture): selling a spare attachment asks in place
|
"""Server texts in the confirms (needs --fixture): selling an idle drill asks in place
|
||||||
(cards.ask_button), buy seat, buy refinery slot and start a new game open the server's
|
(cards.ask_button), buy seat, buy refinery slot and start a new game open the server's
|
||||||
confirm dialog (each cancelled with Esc, nothing bought); the theme toggle switches live.
|
confirm dialog (each cancelled with Esc, nothing bought); the theme toggle switches live.
|
||||||
Shots of each."""
|
Shots of each."""
|
||||||
@@ -44,9 +44,9 @@ def run(game, args):
|
|||||||
fx = game.fixture
|
fx = game.fixture
|
||||||
pre = (args[0] + "-") if args else ""
|
pre = (args[0] + "-") if args else ""
|
||||||
game.idle()
|
game.idle()
|
||||||
game.navigate("{section='equipment', drill='%s'}" % fx["drill_mining"])
|
game.navigate("{section='equipment', drill='%s'}" % fx["tool_idle"])
|
||||||
game.idle()
|
game.idle()
|
||||||
ask(game, "confirm:sell-attachment", pre + "texts-sell-attachment")
|
ask(game, "confirm:sell-tool", pre + "texts-sell-tool")
|
||||||
game.navigate("{section='advisors'}")
|
game.navigate("{section='advisors'}")
|
||||||
game.idle()
|
game.idle()
|
||||||
confirm(game, "confirm:buy-seat", pre + "texts-buy-seat")
|
confirm(game, "confirm:buy-seat", pre + "texts-buy-seat")
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Drill upgrade tracks (needs --fixture): the Equipment cards' two track boxes (yield and
|
||||||
|
cycle, side by side: level, value ▸ next, Upgrade or Max level, the kept need line) and the
|
||||||
|
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).
|
||||||
|
Shots: tracks-equipment, tracks-panel, tracks-panel-after. Args: an optional name prefix."""
|
||||||
|
|
||||||
|
|
||||||
|
def ups(game):
|
||||||
|
return sorted(e["id"] for e in game.elements() if e["id"].startswith("act:upgrade?"))
|
||||||
|
|
||||||
|
|
||||||
|
def run(game, args):
|
||||||
|
fx = game.fixture
|
||||||
|
pre = (args[0] + "-") if args else ""
|
||||||
|
game.idle()
|
||||||
|
game.navigate("{section='equipment'}")
|
||||||
|
game.idle()
|
||||||
|
game.shot(pre + "tracks-equipment", settle=1.0)
|
||||||
|
game.lint(pre + "tracks-equipment")
|
||||||
|
|
||||||
|
game.navigate("{section='equipment', drill='%s'}" % fx["tool_idle"])
|
||||||
|
game.idle()
|
||||||
|
game.shot(pre + "tracks-panel", settle=1.0)
|
||||||
|
game.lint(pre + "tracks-panel")
|
||||||
|
tool = game.eval("g.state.equipment.drill.toolId")
|
||||||
|
for kind in ("yield", "cycle"):
|
||||||
|
ids = [i for i in ups(game) if ("tool=" + tool) in i and i.endswith("track=" + kind)]
|
||||||
|
game.expect(ids, "%s upgrade button in the panel: %s" % (kind, ups(game)))
|
||||||
|
for n in range(2):
|
||||||
|
ids = [i for i in ups(game) if ("tool=" + tool) in i and i.endswith("track=" + kind)]
|
||||||
|
if not ids:
|
||||||
|
break # (the top level: Max level in its place)
|
||||||
|
game.click_check(ids[-1], name=pre + "tracks-click-%s-%d" % (kind, n))
|
||||||
|
game.shot(pre + "tracks-panel-after", settle=1.0)
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""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:
|
||||||
|
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
|
||||||
|
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
|
||||||
|
idle slot with Redrill (cards-idle), Redrill; selling the moon is in the main panel. The
|
||||||
|
aside, with a well and with none selected, has no buttons (aside-well, aside-moon).
|
||||||
|
Args: an optional name prefix."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
from lists import admin, call # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def aside_buttons(game):
|
||||||
|
"""Registered elements drawn inside the aside panel (below the header)."""
|
||||||
|
box = game.eval("(function() local p = g.aside_panel and g.aside_panel.root if not p then return nil end "
|
||||||
|
"local s = gui.get_screen_position(p) return {s.x, s.y} end)()")
|
||||||
|
if not box:
|
||||||
|
return []
|
||||||
|
left, top = box[0], box[1]
|
||||||
|
out = []
|
||||||
|
for e in game.elements():
|
||||||
|
r = e.get("rect")
|
||||||
|
if not r or not e.get("visible") or e.get("kind") == "wait":
|
||||||
|
continue
|
||||||
|
if r["x"] >= left - 1 and r["y"] <= top + 1:
|
||||||
|
out.append(e["id"])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def ids(game, prefix):
|
||||||
|
return [e["id"] for e in game.elements() if e["id"].startswith(prefix) and e.get("visible", True)]
|
||||||
|
|
||||||
|
|
||||||
|
def run(game, args):
|
||||||
|
pre = (args[0] + "-") if args else ""
|
||||||
|
game.idle()
|
||||||
|
admin(game, "credits", amount=100000, op="grant")
|
||||||
|
had = {m["id"] for m in call(game, "GET", "/api/state?catalog=0")["state"]["moons"]}
|
||||||
|
r = call(game, "POST", "/api/act/buy-moon", {})
|
||||||
|
game.expect(r.get("ok"), "buy a moon: %s" % r.get("error"))
|
||||||
|
st = call(game, "GET", "/api/state?catalog=0")["state"]
|
||||||
|
moon = next(m for m in st["moons"] if m["id"] not in had)
|
||||||
|
wells = [w["id"] for w in moon["wells"]]
|
||||||
|
game.navigate("{section='moons', moon='%s'}" % moon["id"])
|
||||||
|
game.idle()
|
||||||
|
game.wait_for("st.moon and st.moon.id == '%s'" % moon["id"], 10)
|
||||||
|
game.idle()
|
||||||
|
game.shot(pre + "cards-empty", settle=0.8)
|
||||||
|
game.lint(pre + "cards-empty")
|
||||||
|
|
||||||
|
# Pick the last tile in the tray (one-well), deploy it from the first well's slot.
|
||||||
|
tiles = ids(game, "pick:")
|
||||||
|
game.expect(len(tiles) > 0, "the tray has idle drills")
|
||||||
|
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.expect(game.eval("g.deploy_pick ~= nil"), "a tile picks")
|
||||||
|
game.shot(pre + "cards-picked", settle=0.5)
|
||||||
|
slot = "act:stake?target=" + wells[0]
|
||||||
|
game.scroll_to(ids(game, slot)[0])
|
||||||
|
game.click_check(ids(game, slot)[0], name=pre + "click-deploy")
|
||||||
|
if ids(game, slot):
|
||||||
|
game.click_check(ids(game, slot)[0], name=pre + "click-deploy-2")
|
||||||
|
game.idle()
|
||||||
|
|
||||||
|
# A multi-well drill (bought when none is idle).
|
||||||
|
st = call(game, "GET", "/api/state")["state"]
|
||||||
|
wide = [t for t in st["tools"] if not t.get("stakeId") and (t.get("reach") or 1) > 1]
|
||||||
|
if not wide:
|
||||||
|
item = next((t for t in st["catalog"]["tools"] if (t.get("reach") or 1) > 1), None)
|
||||||
|
if item:
|
||||||
|
call(game, "POST", "/api/act/buy-tool", {"id": str(item["storeItemId"])})
|
||||||
|
st = call(game, "GET", "/api/state?catalog=0")["state"]
|
||||||
|
wide = [t for t in st["tools"] if not t.get("stakeId") and (t.get("reach") or 1) > 1]
|
||||||
|
if wide:
|
||||||
|
r = call(game, "POST", "/api/act/stake", {"tool": wide[0]["id"], "target": wells[-1]})
|
||||||
|
game.expect(r.get("ok"), "stake a multi-well drill: %s" % r.get("error"))
|
||||||
|
# One more one-well drill on another well, so a card has two drill slots.
|
||||||
|
one = [t for t in st["tools"] if not t.get("stakeId") and (t.get("reach") or 1) == 1]
|
||||||
|
if one and len(wells) > 1:
|
||||||
|
call(game, "POST", "/api/act/stake", {"tool": one[0]["id"], "target": wells[1]})
|
||||||
|
game.navigate("{section='moons', moon='%s', well='%s'}" % (moon["id"], wells[0]))
|
||||||
|
game.idle()
|
||||||
|
game.shot(pre + "cards-mining", settle=0.8)
|
||||||
|
game.lint(pre + "cards-mining")
|
||||||
|
left = aside_buttons(game)
|
||||||
|
game.expect(not left, "the aside (a well) has no buttons: %s" % left)
|
||||||
|
game.shot(pre + "aside-well", settle=0.3)
|
||||||
|
|
||||||
|
# Finish them: ready slots.
|
||||||
|
admin(game, "finish-stakes")
|
||||||
|
game.navigate("{section='moons', moon='%s'}" % moon["id"])
|
||||||
|
game.idle()
|
||||||
|
game.wait_for("(function() for _, w in ipairs(st.moon.wells) do if w.ready > 0 then return true end end end)()", 15)
|
||||||
|
game.idle()
|
||||||
|
game.shot(pre + "cards-ready", settle=0.8)
|
||||||
|
game.lint(pre + "cards-ready")
|
||||||
|
claims = [i for i in ids(game, "act:claim?claim-well=" + wells[0]) if "redeploy" not in i]
|
||||||
|
game.expect(len(claims) > 0, "a ready slot has Claim")
|
||||||
|
game.click_check(claims[0], name=pre + "click-claim")
|
||||||
|
game.wait_for("not g.busy and #g.pending == 0", 20)
|
||||||
|
game.idle()
|
||||||
|
game.shot(pre + "cards-idle", settle=0.8)
|
||||||
|
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])
|
||||||
|
game.expect(len(redrill) > 0 or not left_here, "the idle slot has Redrill (the well has %s left)" % left_here)
|
||||||
|
if redrill:
|
||||||
|
game.click(redrill[0])
|
||||||
|
game.wait_for("not g.busy and #g.pending == 0", 20)
|
||||||
|
game.idle()
|
||||||
|
game.expect(ids(game, "moon-sale:" + moon["id"]) or ids(game, "sell-moon"), "selling the moon is in the main panel")
|
||||||
|
game.shot(pre + "cards-redrilled", settle=0.8)
|
||||||
|
left = aside_buttons(game)
|
||||||
|
game.expect(not left, "the aside (the moon) has no buttons: %s" % left)
|
||||||
|
game.shot(pre + "aside-moon", settle=0.3)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""The well panel scrolled down (needs --fixture): deploy cards with pictures, pips and
|
"""The well panel scrolled down (needs --fixture; it only shows, deploying is on the well
|
||||||
reach, drill lines with thumbnails and attachment tags, the drills heading's share note;
|
cards): drill lines with thumbnails and level tags, the drills heading's share note;
|
||||||
then the market (movers, quote values, Refined tag) and a past moon in the list."""
|
then the market (movers, quote values, Refined tag) and a past moon in the list."""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,3 @@ sql/queries.sql
|
|||||||
# Generated by the art scripts
|
# Generated by the art scripts
|
||||||
/art/equipment/build/
|
/art/equipment/build/
|
||||||
/art/advisors/build/
|
/art/advisors/build/
|
||||||
/art/attachments/build/
|
|
||||||
+23
-22
@@ -49,7 +49,7 @@ shared rules: `../AGENTS.md`. Run every command below from this directory (`serv
|
|||||||
map its view model in `addSection`. `api_test.go` (in `make check`) builds every block
|
map its view model in `addSection`. `api_test.go` (in `make check`) builds every block
|
||||||
for every test view.
|
for every test view.
|
||||||
- `config/game.json`: the game config (starting credits/resources/tools, `moonGrades`,
|
- `config/game.json`: the game config (starting credits/resources/tools, `moonGrades`,
|
||||||
resource prices, store items with upgrade tiers, moon types: a name, a placeholder
|
resource prices, store items with their two upgrade tracks, moon types: a name, a placeholder
|
||||||
description and resource `affinities`, refinery), embedded. `moonGrades` (D, C, B, A, S,
|
description and resource `affinities`, refinery), embedded. `moonGrades` (D, C, B, A, S,
|
||||||
cheapest first) are moon vendors: a price, `wellCountWeights`, a well supply range and
|
cheapest first) are moon vendors: a price, `wellCountWeights`, a well supply range and
|
||||||
affinities that multiply the type's. The grade is `staking_source.grade` (migration 018;
|
affinities that multiply the type's. The grade is `staking_source.grade` (migration 018;
|
||||||
@@ -58,11 +58,11 @@ shared rules: `../AGENTS.md`. Run every command below from this directory (`serv
|
|||||||
count by its grade's weights, resources by the affinities, each well's supply in the
|
count by its grade's weights, resources by the affinities, each well's supply in the
|
||||||
grade's [`wellSupplyMin`, `wellSupplyMax`); a first moon always has every raw input of a
|
grade's [`wellSupplyMin`, `wellSupplyMax`); a first moon always has every raw input of a
|
||||||
recipe. Ratings, reveal bars, resale and descriptions compare a moon with its grade.
|
recipe. Ratings, reveal bars, resale and descriptions compare a moon with its grade.
|
||||||
Upgrade tiers set a cycle `speed` (read from cfg(), not stored; `StoreItem.ToolSecs`). A moon's name comes from `moonNames` (roots and
|
Track levels set a tool's yield and cycle (read from cfg(), not stored; `StoreItem.ToolYield`, `ToolSecs`). A moon's name comes from `moonNames` (roots and
|
||||||
suffix style weights, `rollMoonName`), never repeated among a player's moons; its type is
|
suffix style weights, `rollMoonName`), never repeated among a player's moons; its type is
|
||||||
`staking_source.kind` (moons from before migration 013 were named after their type).
|
`staking_source.kind` (moons from before migration 013 were named after their type).
|
||||||
`config.go` parses it into `game` and syncs store items into `store_item`/`upgrade_item`
|
`config.go` parses it into `game` and syncs store items into `store_item`/`upgrade_item`
|
||||||
(needed for the inventory foreign key and the SQL in game.go). Admins override its
|
(needed for the inventory foreign key and the SQL in game.go; the tracks aren't stored). Admins override its
|
||||||
values live on `/utils/economy` (table editor in `economy.js`; `economy.go`: a patch per
|
values live on `/utils/economy` (table editor in `economy.js`; `economy.go`: a patch per
|
||||||
save in `config_override`, latest wins; `sim.go`: balance analysis and progression sim). Read config values through
|
save in `config_override`, latest wins; `sim.go`: balance analysis and progression sim). Read config values through
|
||||||
`cfg()` (game.json + override), never `game` directly. `migrate.go`: database setup.
|
`cfg()` (game.json + override), never `game` directly. `migrate.go`: database setup.
|
||||||
@@ -87,25 +87,27 @@ shared rules: `../AGENTS.md`. Run every command below from this directory (`serv
|
|||||||
at `maxRatio` × the replacement moon's price (`moonPriceFor(owned-1)`), which it's
|
at `maxRatio` × the replacement moon's price (`moonPriceFor(owned-1)`), which it's
|
||||||
compared with. Not the last moon, not within `minHoldSecs` of buying it, not while a
|
compared with. Not the last moon, not within `minHoldSecs` of buying it, not while a
|
||||||
drill on it is running; finished drills are claimed in the sale. Template: `moon-sale`.
|
drill on it is running; finished drills are claimed in the sale. Template: `moon-sale`.
|
||||||
Also selling tools and spare attachments for `equipmentResale.factor` (below 1) of their
|
Also selling tools for `equipmentResale.factor` (below 1) of their credit price (price +
|
||||||
credit price (a tool: price + its upgrades; refined parts not refunded): only idle
|
its track levels; refined parts not refunded): only idle tools with nothing to claim,
|
||||||
tools with nothing to claim, never the last tool; attachments on it return to the inventory.
|
never the last tool.
|
||||||
- `refinery.go`: refinery config (`refinery` in game.json, `parts` on upgrade tiers), rules
|
- `refinery.go`: refinery config (`refinery` in game.json, `parts` on track levels), rules
|
||||||
(start batch, collect, buy slot, batch size upgrade, sell refined, upgrade parts) and its
|
(start batch, collect, buy slot, batch size upgrade, sell refined, upgrade parts) and its
|
||||||
view model. A batch of n takes `seconds` × min(n, 1 + `bulkLog` × ln n) (`batchSecs`): logarithmic. The most per
|
view model. A batch of n takes `seconds` × min(n, 1 + `bulkLog` × ln n) (`batchSecs`): logarithmic. The most per
|
||||||
batch is `maxBatch`, raised for the whole refinery (every slot) by `batchUpgrades`
|
batch is `maxBatch`, raised for the whole refinery (every slot) by `batchUpgrades`
|
||||||
(action `upgrade-batch`, `users.refinery_batch_level`). Tables
|
(action `upgrade-batch`, `users.refinery_batch_level`). Tables
|
||||||
`refined_account`, `refinery_job`, `users.refinery_slots`. Template: `refinery.html`.
|
`refined_account`, `refinery_job`, `users.refinery_slots`. Template: `refinery.html`.
|
||||||
- `bonus.go`: player bonuses. `bonusesFor` combines advisors and a tool's attachments
|
- `bonus.go`: player bonuses. `bonusesFor` combines the seated advisors (multipliers multiply, then each kind is capped by `bonusCaps`); live events multiply on
|
||||||
(multipliers multiply, then each kind is capped by `bonusCaps`); live events multiply on
|
top, uncapped. `drillStats` turns a tool's cycle and yield into a stake's (cycles to a tenth of a second, at least 1 s) (used by
|
||||||
top, uncapped. `drillStats` turns a tool's cycle and yield into a stake's (used by
|
|
||||||
`stakeInTx`, the views and the sim); sells and refinery batches use `Bonus.Sell`/`Refine`.
|
`stakeInTx`, the views and the sim); sells and refinery batches use `Bonus.Sell`/`Refine`.
|
||||||
Bonuses are fixed when the action runs (a stake keeps its duration and amount).
|
Bonuses are fixed when the action runs (a stake keeps its duration and amount).
|
||||||
- `attachments.go`: attachments config (`attachments` in game.json: `slotsPerTool`, items
|
- `tracks.go`: drill upgrade tracks. Each store item has two named tracks (`tracks.yield`,
|
||||||
with `speed`/`yield` multipliers), buy/equip/unequip rules (one of each per tool; equip
|
`tracks.cycle` in game.json): levels that set the claim per cycle or the cycle in seconds
|
||||||
and unequip any time, they count from the next stake), store cards and the drill panel
|
flat, with a price and, higher up, refined `parts`. Levels are per tool
|
||||||
(`drill=` in the URL, Equipment section, in the aside). Table `attachment`
|
(`inventory_item.yield_level`, `cycle_level`; copies with the same levels are
|
||||||
(`inventory_item_id` null: unequipped).
|
interchangeable, `pickTool`); action `upgrade` (`track=yield|cycle`), counted from the next
|
||||||
|
stake. Reach is the item's and never changes. Also the view models (`TrackRow` with its
|
||||||
|
ladder, on the Equipment card and in the drill panel: `drill=` in the URL, Equipment
|
||||||
|
section, in the aside; the toast text).
|
||||||
- `advisors.go`, `web/templates/advisors.html`: advisors config (`advisors`), hire, buy
|
- `advisors.go`, `web/templates/advisors.html`: advisors config (`advisors`), hire, buy
|
||||||
seat, seat/unseat/swap/dismiss rules (only seated advisors give a bonus; dismissing
|
seat, seat/unseat/swap/dismiss rules (only seated advisors give a bonus; dismissing
|
||||||
refunds nothing), Advisors section and the advisor panel (`adv=` in the URL, in the
|
refunds nothing), Advisors section and the advisor panel (`adv=` in the URL, in the
|
||||||
@@ -114,8 +116,8 @@ shared rules: `../AGENTS.md`. Run every command below from this directory (`serv
|
|||||||
holds flags that are only ever added (gates passed, `claimed:<resource>`, `seen:<tab>`,
|
holds flags that are only ever added (gates passed, `claimed:<resource>`, `seen:<tab>`,
|
||||||
`all` for players from before it). New players see only the first moon purchase; deploying
|
`all` for players from before it). New players see only the first moon purchase; deploying
|
||||||
reveals Equipment, claiming Market, selling Store (and "+ New moon", Help, upgrades,
|
reveals Equipment, claiming Market, selling Store (and "+ New moon", Help, upgrades,
|
||||||
attachments, moon sale, Redrill), buying a moon of `refinery.unlockGrade` (C) or higher Refinery, buying a
|
moon sale, Redrill), buying a moon of `refinery.unlockGrade` (C) or higher Refinery, buying a
|
||||||
tool or attachment Advisors. Templates ask `.S.Unlocked "<feature>"`; `View.resolve` sends
|
tool Advisors. Templates ask `.S.Unlocked "<feature>"`; `View.resolve` sends
|
||||||
locked sections to the moon view and `runAction` refuses locked actions (`actionGates`).
|
locked sections to the moon view and `runAction` refuses locked actions (`actionGates`).
|
||||||
A revealed tab shines (`data-shine`) until opened (`markSeen` in `handleIndex`). New
|
A revealed tab shines (`data-shine`) until opened (`markSeen` in `handleIndex`). New
|
||||||
sections and actions need a gate there.
|
sections and actions need a gate there.
|
||||||
@@ -152,9 +154,8 @@ shared rules: `../AGENTS.md`. Run every command below from this directory (`serv
|
|||||||
- `liveops.go`, `events.go`, `web/templates/liveops.html`, `web/static/js/liveops.js`: live
|
- `liveops.go`, `events.go`, `web/templates/liveops.html`, `web/static/js/liveops.js`: live
|
||||||
ops at `/utils/liveops` (dashboard from `event_log`, timed events in `live_event` applied
|
ops at `/utils/liveops` (dashboard from `event_log`, timed events in `live_event` applied
|
||||||
via `eventMultiplier` in game.go, `announcement`; both shown in the game's `#banner`).
|
via `eventMultiplier` in game.go, `announcement`; both shown in the game's `#banner`).
|
||||||
- `scripts/tool-art.mjs`: generated tool art per tier, not in use (its output was removed;
|
- `scripts/tool-art.mjs`: generated tool art per tier (from the old tier ladder), not in
|
||||||
the tools show their original images). `StoreItem.ImageAt` uses `<image>-tN` from
|
use; the tools show their original images.
|
||||||
`web/static/assets/items` when it exists, else the base image.
|
|
||||||
- `web/css/app.css`: Tailwind input (custom HUD styles); builds to `web/static/css/app.css`.
|
- `web/css/app.css`: Tailwind input (custom HUD styles); builds to `web/static/css/app.css`.
|
||||||
- `web/app/` (paused experiment: the Preact client, `GET /app`, template `spa.html`): the
|
- `web/app/` (paused experiment: the Preact client, `GET /app`, template `spa.html`): the
|
||||||
Moons slice on the JSON API; other sections link to the classic client. `session.js` is
|
Moons slice on the JSON API; other sections link to the classic client. `session.js` is
|
||||||
@@ -164,10 +165,10 @@ shared rules: `../AGENTS.md`. Run every command below from this directory (`serv
|
|||||||
effects (`scripts/check-signals-jsx.mjs` in `make check`). Key list items by stable ids
|
effects (`scripts/check-signals-jsx.mjs` in `make check`). Key list items by stable ids
|
||||||
(drills by tool id, not stake id); never swap an element that animates for another.
|
(drills by tool id, not stake id); never swap an element that animates for another.
|
||||||
Animate with transform/opacity (CSS, Web Animations), FLIP (`flip.js`) for rows that move.
|
Animate with transform/opacity (CSS, Web Animations), FLIP (`flip.js`) for rows that move.
|
||||||
- `sql/tables.sql`, `sql/data.sql`: schema for fresh databases. `sql/migrations/` (001–019):
|
- `sql/tables.sql`, `sql/data.sql`: schema for fresh databases. `sql/migrations/` (001–020):
|
||||||
changes for existing ones, applied on every `migrate`, so each must be idempotent.
|
changes for existing ones, applied on every `migrate`, so each must be idempotent.
|
||||||
- `test-endpoints.restclient`: the up-to-date list of endpoints, in Emacs restclient format.
|
- `test-endpoints.restclient`: the up-to-date list of endpoints, in Emacs restclient format.
|
||||||
- `art/`: sources of the store, attachment and advisor images (Blender scripts, prompts);
|
- `art/`: sources of the store and advisor images (Blender scripts, prompts);
|
||||||
they write into `web/static/assets`.
|
they write into `web/static/assets`.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|||||||
@@ -1,3 +1,57 @@
|
|||||||
|
# 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
|
||||||
|
before the next grade, cheaper than that grade's moons) empties the current grade's wells
|
||||||
|
in about one claim; on the next grade it is back to 3–6. Each drill's base yield is about
|
||||||
|
its grade's average well ÷ 4.5 (multi-well drills × their reach): PD-1 8 (D), PX-4 60 (C),
|
||||||
|
L100 300 (B), L300 4,000 (A), LRE 48,000 (S); EDE 110,000 and LCE 200,000 rip S further
|
||||||
|
(more drills than grades until planets and stars). Sim, claims per average well on its own
|
||||||
|
grade: PD-1 4.3, PX-4 5.4, L100 4.5, L300 4.5, LRE 4.6, EDE 3.3, LCE 1.8.
|
||||||
|
|
||||||
|
Prices: each drill earns a bit more per credit than the one before on its grade (sim "a
|
||||||
|
drill alone on its grade", ₥/min per 1k₥: 981, 1.6k, 2k, 2.4k, 2.6k, 2.9k, 3.1k), else the
|
||||||
|
sim (and a player) just buys more of the cheapest drill: PD-1 1,200, PX-4 4,000, L100
|
||||||
|
13,400, L300 129,000, LRE 1.18M, EDE 2.12M, LCE 3.1M. Each upgrade level costs 0.95 × what
|
||||||
|
a copy would cost for the same added output (so upgrades are slightly better per credit).
|
||||||
|
|
||||||
|
Sim, first moon of each grade: copies C 7m, B 15m, A 32m, S 1h04; boosts C 6m, B 14m,
|
||||||
|
A 31m, S 49m (was about C 13m, B 22m, A 1h10, S 3h). Faster overall; the lever for a longer
|
||||||
|
game is moon and drill prices with wells scaled alike.
|
||||||
|
|
||||||
|
# Drill upgrade tracks (2026-10-22)
|
||||||
|
|
||||||
|
Attachments and the T1–T5 tier ladder are gone. Each drill has two tracks of its own
|
||||||
|
(config `storeItems[].tracks`), 4–5 levels each, every level a flat value: yield (claim per
|
||||||
|
cycle) and cycle (seconds, to a tenth). Reach never changes. Levels 3+ need refined parts.
|
||||||
|
Prices: level k of yield costs 0.97 × drill price × (Δclaim / base claim) × (base cycle /
|
||||||
|
cycle at level k−1), cycle level k costs 0.97 × price × (claim at level k−1 / base) × Δ(base
|
||||||
|
cycle / cycle), × 0.95^(k−1), minus the parts' value, two significant digits, never below
|
||||||
|
the level before. So with both tracks moving together a level adds output for about a
|
||||||
|
copy's price.
|
||||||
|
|
||||||
|
| drill | yield track | cycle track |
|
||||||
|
|---|---|---|
|
||||||
|
| PD-1 | Twin rotors 4 → 5, 6, 7, 8 | Flight computer 3 → 2.6, 2.2, 1.9, 1.7, 1.5 s |
|
||||||
|
| PX-4 | Wider chisel 20 → 24 … 40 | Pneumatic hammer 6 → 5.2, 4.5, 3.9, 3.4, 3 s |
|
||||||
|
| L100 | Wide-bore bit 110 → 130 … 220 | Spindle motor 8 → 7, 6, 5.2, 4.6, 4 s |
|
||||||
|
| L300 | Wider beam 1350 → 1600 … 2700 | Laser capacitors 10 → 8.7 … 5 s |
|
||||||
|
| LRE | Fine sieves 17k → 20k … 34k | Auger gearbox 12 → 10.4 … 6 s |
|
||||||
|
| EDE | Larger plates 36k → 43k … 72k | Charge coils 14 → 12.2 … 7 s |
|
||||||
|
| LCE | Core barrel 47k → 56k … 94k | Drill string 16 → 14 … 8 s |
|
||||||
|
|
||||||
|
Per ₥ against a copy (a drill alone on its grade): the first yield level ×1.02–1.05 (₥/min),
|
||||||
|
×1.04–1.09 (₥/click); whole yield track ×0.79–0.88; first cycle level ×0.94–1.01 (₥/click
|
||||||
|
×0.28–0.67: shorter cycles cost claims); both tracks to the top ×1.12–1.19. PD-1's cycle
|
||||||
|
levels add nothing in the sim: its player claims at most every 3 s (`gsClaimGap`).
|
||||||
|
|
||||||
|
| first moon of each grade | before (tiers, attachments) | after (tracks) |
|
||||||
|
|---|---|---|
|
||||||
|
| copies | C 13m15, B 22m26, A 1h10m37, S 2h59m52 | the same |
|
||||||
|
| boosts | C 13m15, B 22m26, A 1h08m39, S 3h00m21 (bought only copies and 2 advisors) | C 13m18, B 23m39, A 1h05m50, S 3h01m59 (233 copies, 234 levels, 2 advisors) |
|
||||||
|
|
||||||
|
At 0.85 (levels ~15% better than a copy) the boosts player reached A at 54 m and S at
|
||||||
|
2h26; at 1.0 and above it bought almost no levels.
|
||||||
|
|
||||||
# Balance notes: moon grades, clicker pacing (2026-10-21)
|
# Balance notes: moon grades, clicker pacing (2026-10-21)
|
||||||
|
|
||||||
Moon Miners is a clicker: the player is always clicking. `go run . sim` prints the grade
|
Moon Miners is a clicker: the player is always clicking. `go run . sim` prints the grade
|
||||||
|
|||||||
+13
-11
@@ -4,7 +4,7 @@ A browser mining game. Players own moons (staking sources) with resource wells,
|
|||||||
upgrade mining tools, stake them on wells, claim resources when the stake completes, and sell
|
upgrade mining tools, stake them on wells, claim resources when the stake completes, and sell
|
||||||
resources for credits (₥). Drill cycles last seconds, so play is active. The refinery turns
|
resources for credits (₥). Drill cycles last seconds, so play is active. The refinery turns
|
||||||
resources into refined items on a timer; those sell for more and are needed as parts for
|
resources into refined items on a timer; those sell for more and are needed as parts for
|
||||||
higher tool upgrades.
|
the higher levels of the drills' upgrade tracks.
|
||||||
|
|
||||||
It's a Go server that renders the interface as HTML (with htmx for in-place updates) next to
|
It's a Go server that renders the interface as HTML (with htmx for in-place updates) next to
|
||||||
a three.js view of the selected moon, generated procedurally from its id. Game rules run in
|
a three.js view of the selected moon, generated procedurally from its id. Game rules run in
|
||||||
@@ -93,17 +93,21 @@ same schema (compare `pg_dump --schema-only`). The migrations so far: 001 drops
|
|||||||
tables, 002 moon settings, 003 config overrides, 004 live ops, 005 refinery, 006 player
|
tables, 002 moon settings, 003 config overrides, 004 live ops, 005 refinery, 006 player
|
||||||
admin, 007 drill cycles in seconds, 008 security fixes (UTC defaults, one claim per stake,
|
admin, 007 drill cycles in seconds, 008 security fixes (UTC defaults, one claim per stake,
|
||||||
non-negative well supply), 009 schema cleanup (old columns, moon images), 010 drill
|
non-negative well supply), 009 schema cleanup (old columns, moon images), 010 drill
|
||||||
attachments and advisors, 011 market pressure, 012 onboarding, 013 moon type
|
attachments (dropped in 020) and advisors, 011 market pressure, 012 onboarding, 013 moon type
|
||||||
(`staking_source.kind`, now that moons get generated names), 014 drill reach, 015 moon
|
(`staking_source.kind`, now that moons get generated names), 014 drill reach, 015 moon
|
||||||
rating, 016 advisor seats, 017 save codes, 018 moon grade, 019 refinery batch size
|
rating, 016 advisor seats, 017 save codes, 018 moon grade, 019 refinery batch size
|
||||||
(`users.refinery_batch_level`).
|
(`users.refinery_batch_level`), 020 drill upgrade tracks (`inventory_item.yield_level`,
|
||||||
|
`cycle_level`; old tiers became cycle levels, attachments were refunded and dropped,
|
||||||
|
`upgrade_item` dropped, stake cycles in tenths of a second).
|
||||||
|
|
||||||
## Game config
|
## Game config
|
||||||
|
|
||||||
`config/game.json` holds the balance numbers (starting credits, resources and tools, moon
|
`config/game.json` holds the balance numbers (starting credits, resources and tools, moon
|
||||||
price, well supply range, `wellCountWeights`: how many wells a new moon gets, resource prices
|
price, well supply range, `wellCountWeights`: how many wells a new moon gets, resource prices
|
||||||
in credits per unit), the store items with their
|
in credits per unit), the store items with their
|
||||||
upgrade tiers (tiers can also need refined `parts`; a drill cycle is `completionTimeInSecs`),
|
two upgrade tracks (`tracks.yield` and `tracks.cycle`: a name and levels, each setting the
|
||||||
|
claim per cycle or the cycle in seconds flat, with a `price` and, higher up, refined `parts`;
|
||||||
|
a drill's base cycle is `completionTimeInSecs`, its reach never changes),
|
||||||
the moon types (a name, a placeholder description: the look is procedural and the
|
the moon types (a name, a placeholder description: the look is procedural and the
|
||||||
description is generated when the moon is bought, see `moondesc.go`, and `affinities`:
|
description is generated when the moon is bought, see `moondesc.go`, and `affinities`:
|
||||||
weights of the resources its wells hold, 1 if unlisted), moon names (`moonNames`: roots, and
|
weights of the resources its wells hold, 1 if unlisted), moon names (`moonNames`: roots, and
|
||||||
@@ -117,11 +121,9 @@ function of name and time (`market.go`), which also gives the 24h charts; sells
|
|||||||
A player's own selling lowers their price a little (`marketPressure`): each sale adds its
|
A player's own selling lowers their price a little (`marketPressure`): each sale adds its
|
||||||
base value to their pressure on the item, which halves every `halfLifeSecs`, and the price
|
base value to their pressure on the item, which halves every `halfLifeSecs`, and the price
|
||||||
factor is `floor + (1 − floor) × e^(−pressure / scale)`, so it flattens out at `floor`.
|
factor is `floor + (1 − floor) × e^(−pressure / scale)`, so it flattens out at `floor`.
|
||||||
Drill attachments (`attachments`: `slotsPerTool`, and per attachment a `price`, an `image`
|
Each drill's tracks are upgraded per tool, one level at a time, on its Equipment card or
|
||||||
and `speed`/`yield` multipliers) are bought in the Store and equipped on a drill from its
|
in its panel, where idle drills can also be sold back for `equipmentResale.factor` of their
|
||||||
panel in Equipment, where idle drills and spare attachments can also be sold back for
|
credit price (its price plus its track levels; refined parts aren't refunded). A moon sells back (`moonResale`) for what's left in its wells at market
|
||||||
`equipmentResale.factor` of their credit price (a drill: its price plus its upgrades; refined
|
|
||||||
parts aren't refunded). A moon sells back (`moonResale`) for what's left in its wells at market
|
|
||||||
price × `factor`, at most `maxRatio` × the price of a replacement moon, no sooner than
|
price × `factor`, at most `maxRatio` × the price of a replacement moon, no sooner than
|
||||||
`minHoldSecs` after buying it, and not while a drill on it is running. Advisors (`advisors`: `seatPrices` for the 2nd, 3rd, ... seat, and per
|
`minHoldSecs` after buying it, and not while a drill on it is running. Advisors (`advisors`: `seatPrices` for the 2nd, 3rd, ... seat, and per
|
||||||
advisor a hire `price` and `speed`/`yield`/`sell`/`refine` multipliers) boost the whole
|
advisor a hire `price` and `speed`/`yield`/`sell`/`refine` multipliers) boost the whole
|
||||||
@@ -147,7 +149,7 @@ shares one nav: Players · Moons · Economy · Live ops · Game.
|
|||||||
| `/utils/players` | Find players (id prefix or name), sort by last seen, activity, newest, richest. |
|
| `/utils/players` | Find players (id prefix or name), sort by last seen, activity, newest, richest. |
|
||||||
| `/utils/players/{id}` | One player: balance, resources, refined items, moons and wells, tools and drills, event history (older pages at `/utils/players/{id}/events?page=n`). Grant/remove credits, grant resources and tools, finish running drills, reset, ban/unban. |
|
| `/utils/players/{id}` | One player: balance, resources, refined items, moons and wells, tools and drills, event history (older pages at `/utils/players/{id}/events?page=n`). Grant/remove credits, grant resources and tools, finish running drills, reset, ban/unban. |
|
||||||
| `/utils/moons` | Moon lab: live 3D preview with sliders for terrain, craters, colors, bloom, atmosphere and rotation. "Save for the game" applies the look to every moon. |
|
| `/utils/moons` | Moon lab: live 3D preview with sliders for terrain, craters, colors, bloom, atmosphere and rotation. "Save for the game" applies the look to every moon. |
|
||||||
| `/utils/economy` | Edit the game config without a deploy: tables for equipment (with upgrade tiers), resources, refined items, market, attachments, advisors, moon types and constants, saved together as a draft; live analysis of the draft, version history with revert/reset, raw JSON. |
|
| `/utils/economy` | Edit the game config without a deploy: tables for equipment (with its yield and cycle tracks), resources, refined items, market, advisors, moon types and constants, saved together as a draft; live analysis of the draft, version history with revert/reset, raw JSON. |
|
||||||
| `/utils/liveops` | Dashboard, timed events and announcements. |
|
| `/utils/liveops` | Dashboard, timed events and announcements. |
|
||||||
|
|
||||||
Actions (all `POST`, confirmed in the page's own dialog):
|
Actions (all `POST`, confirmed in the page's own dialog):
|
||||||
@@ -177,7 +179,7 @@ edits the shared settings that every moon starts from; they're stored as one JSO
|
|||||||
|
|
||||||
### Economy
|
### Economy
|
||||||
|
|
||||||
A form for the constants, resource prices, store items and upgrade tiers (including parts),
|
A form for the constants, resource prices, store items and their upgrade tracks (including parts),
|
||||||
and the refinery, plus a raw JSON editor for every field. The analysis updates as you type:
|
and the refinery, plus a raw JSON editor for every field. The analysis updates as you type:
|
||||||
income and payback per tool and upgrade, refining gain per recipe, red flags, and a 72-hour
|
income and payback per tool and upgrade, refining gain per recipe, red flags, and a 72-hour
|
||||||
greedy-player sim. Each save stores the values that differ from `config/game.json` as a new row
|
greedy-player sim. Each save stores the values that differ from `config/game.json` as a new row
|
||||||
|
|||||||
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
// Advisors: people a player hires once, for credits, who boost the whole operation (drill
|
// Advisors: people a player hires once, for credits, who boost the whole operation (drill
|
||||||
// speed, yield, sell prices, refinery speed; bonus.go combines them with drill attachments).
|
// speed, yield, sell prices, refinery speed; bonus.go combines them).
|
||||||
// Seats are numbered: players start with one and buy more (advisors.seatPrices), like
|
// Seats are numbered: players start with one and buy more (advisors.seatPrices), like
|
||||||
// refinery slots. Hiring needs a free seat and seats the advisor in the lowest one. Only
|
// refinery slots. Hiring needs a free seat and seats the advisor in the lowest one. Only
|
||||||
// seated advisors give their bonus. A hired advisor can be unseated (kept on the payroll,
|
// seated advisors give their bonus. A hired advisor can be unseated (kept on the payroll,
|
||||||
// no bonus), seated again in a free seat, swapped with another (seated or not) or
|
// no bonus), seated again in a free seat, swapped with another (seated or not) or
|
||||||
// dismissed. Dismissing refunds nothing (the price is a hiring fee, like seats and slots,
|
// dismissed. Dismissing refunds nothing (the price is a hiring fee, like seats and slots,
|
||||||
// which aren't refunded either); hiring them again costs the full price. Like attachments,
|
// which aren't refunded either); hiring them again costs the full price. Like every bonus,
|
||||||
// changes count from the next action: running drills keep their bonus.
|
// changes count from the next action: running drills keep their bonus.
|
||||||
// Tables advisor_hire (seat null: not seated) and users.advisor_seats.
|
// Tables advisor_hire (seat null: not seated) and users.advisor_seats.
|
||||||
|
|
||||||
|
|||||||
+12
-21
@@ -137,12 +137,11 @@ type APITool struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Code string `json:"code"` // model code, e.g. "PD-1"
|
Code string `json:"code"` // model code, e.g. "PD-1"
|
||||||
Image string `json:"image"` // static URL
|
Image string `json:"image"` // static URL
|
||||||
Tier int `json:"tier"`
|
YieldLevel int `json:"yieldLevel"` // levels on its upgrade tracks (0: base)
|
||||||
MaxTier int `json:"maxTier"`
|
CycleLevel int `json:"cycleLevel"`
|
||||||
Reach int `json:"reach"` // wells one cycle mines
|
Reach int `json:"reach"` // wells one cycle mines
|
||||||
CycleSecs int `json:"cycleSecs"` // a stake now, with bonuses and live events
|
CycleSecs float64 `json:"cycleSecs"` // a stake now, with bonuses and live events (tenths)
|
||||||
Yield float64 `json:"yield"`
|
Yield float64 `json:"yield"`
|
||||||
Attachments []string `json:"attachments"`
|
|
||||||
StakeID string `json:"stakeId,omitempty"` // mining, if set
|
StakeID string `json:"stakeId,omitempty"` // mining, if set
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +155,7 @@ type APIStake struct {
|
|||||||
Reach int `json:"reach"`
|
Reach int `json:"reach"`
|
||||||
Start int64 `json:"start"`
|
Start int64 `json:"start"`
|
||||||
End int64 `json:"end"`
|
End int64 `json:"end"`
|
||||||
Secs int `json:"durationSecs"`
|
Secs float64 `json:"durationSecs"`
|
||||||
Ready bool `json:"ready"`
|
Ready bool `json:"ready"`
|
||||||
Take float64 `json:"take"` // what claiming it would get now
|
Take float64 `json:"take"` // what claiming it would get now
|
||||||
// The web's words for its reach: "Mines 3 wells", "Strips the whole moon", "" for one.
|
// The web's words for its reach: "Mines 3 wells", "Strips the whole moon", "" for one.
|
||||||
@@ -167,7 +166,7 @@ type APIStake struct {
|
|||||||
Gets *float64 `json:"gets,omitempty"`
|
Gets *float64 `json:"gets,omitempty"`
|
||||||
Here *float64 `json:"here,omitempty"`
|
Here *float64 `json:"here,omitempty"`
|
||||||
State string `json:"state,omitempty"`
|
State string `json:"state,omitempty"`
|
||||||
Fate string `json:"fate,omitempty"` // the web's tooltip line, e.g. "Prospector Drone PD-1 T0: +2"
|
Fate string `json:"fate,omitempty"` // the web's tooltip line, e.g. "Prospector Drone PD-1 Y1 C2: +2"
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIDrillGroup struct {
|
type APIDrillGroup struct {
|
||||||
@@ -231,16 +230,16 @@ type APIToolGroup struct {
|
|||||||
ToolID string `json:"toolId"` // send as tool= to stake (an idle copy is picked)
|
ToolID string `json:"toolId"` // send as tool= to stake (an idle copy is picked)
|
||||||
StoreItemID int `json:"storeItemId"`
|
StoreItemID int `json:"storeItemId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Tier int `json:"tier"`
|
YieldLevel int `json:"yieldLevel"`
|
||||||
|
CycleLevel int `json:"cycleLevel"`
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
CycleSecs int `json:"cycleSecs"`
|
CycleSecs float64 `json:"cycleSecs"`
|
||||||
Yield float64 `json:"yield"`
|
Yield float64 `json:"yield"`
|
||||||
Reach int `json:"reach"`
|
Reach int `json:"reach"`
|
||||||
Here float64 `json:"here"` // what a cycle here gets after the drills already on the moon
|
Here float64 `json:"here"` // what a cycle here gets after the drills already on the moon
|
||||||
Free float64 `json:"free"`
|
Free float64 `json:"free"`
|
||||||
Short bool `json:"short"`
|
Short bool `json:"short"`
|
||||||
Blocked bool `json:"blocked"` // here is 0: the stake is refused
|
Blocked bool `json:"blocked"` // here is 0: the stake is refused
|
||||||
Attachments []string `json:"attachments"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIBetterWell struct {
|
type APIBetterWell struct {
|
||||||
@@ -334,14 +333,6 @@ func apiAmounts(rows []ResourceBalance) []APIAmount {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiGear(gear []*Attachment) []string {
|
|
||||||
out := []string{}
|
|
||||||
for _, a := range gear {
|
|
||||||
out = append(out, a.Name)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiStake(st StakeRow, projected bool) APIStake {
|
func apiStake(st StakeRow, projected bool) APIStake {
|
||||||
a := APIStake{ID: st.ID, MoonID: st.MoonID, WellID: st.WellID, ToolID: st.ToolID, Resource: st.Resource,
|
a := APIStake{ID: st.ID, MoonID: st.MoonID, WellID: st.WellID, ToolID: st.ToolID, Resource: st.Resource,
|
||||||
Amount: st.Amount, Reach: max(1, st.Reach), Start: st.Start.UnixMilli(), End: st.End().UnixMilli(),
|
Amount: st.Amount, Reach: max(1, st.Reach), Start: st.Start.UnixMilli(), End: st.End().UnixMilli(),
|
||||||
@@ -415,8 +406,8 @@ func apiState(p *Page, extra ...*Page) *APIState {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tool := APITool{ID: t.ID, StoreItemID: t.StoreItemID, Name: item.Name, Code: toolCode(item.Name),
|
tool := APITool{ID: t.ID, StoreItemID: t.StoreItemID, Name: item.Name, Code: toolCode(item.Name),
|
||||||
Image: itemImages + item.ImageAt(t.Tier), Tier: t.Tier, MaxTier: len(item.Upgrades),
|
Image: itemImages + item.Image, YieldLevel: t.YieldLevel, CycleLevel: t.CycleLevel,
|
||||||
Reach: max(1, item.ToolReach(t.Tier)), Attachments: apiGear(s.gear(t.ID))}
|
Reach: item.ToolReach()}
|
||||||
tool.CycleSecs, tool.Yield = s.drill(t, item, "")
|
tool.CycleSecs, tool.Yield = s.drill(t, item, "")
|
||||||
if st := s.StakeForTool(t.ID); st != nil {
|
if st := s.StakeForTool(t.ID); st != nil {
|
||||||
tool.StakeID = st.ID
|
tool.StakeID = st.ID
|
||||||
@@ -453,8 +444,8 @@ func apiState(p *Page, extra ...*Page) *APIState {
|
|||||||
well := &APIWell{ID: w.ID, Resource: w.Resource, Rate: w.Rate, Idle: []APIToolGroup{}}
|
well := &APIWell{ID: w.ID, Resource: w.Resource, Rate: w.Rate, Idle: []APIToolGroup{}}
|
||||||
for _, g := range w.Idle {
|
for _, g := range w.Idle {
|
||||||
well.Idle = append(well.Idle, APIToolGroup{ToolID: g.ID, StoreItemID: g.StoreItemID, Name: g.Item.Name,
|
well.Idle = append(well.Idle, APIToolGroup{ToolID: g.ID, StoreItemID: g.StoreItemID, Name: g.Item.Name,
|
||||||
Tier: g.Tier, Count: g.Count, CycleSecs: g.Secs, Yield: g.Yield, Reach: max(1, g.Reach), Here: g.Here,
|
YieldLevel: g.YieldLevel, CycleLevel: g.CycleLevel, Count: g.Count, CycleSecs: g.Secs, Yield: g.Yield,
|
||||||
Free: g.Free, Short: g.Short(), Blocked: g.Blocked(), Attachments: apiGear(g.Gear)})
|
Reach: max(1, g.Reach), Here: g.Here, Free: g.Free, Short: g.Short(), Blocked: g.Blocked()})
|
||||||
}
|
}
|
||||||
if b := w.Better; b != nil {
|
if b := w.Better; b != nil {
|
||||||
well.Better = &APIBetterWell{MoonID: b.MoonID, WellID: b.WellID, Resource: b.Resource, Free: b.Free}
|
well.Better = &APIBetterWell{MoonID: b.MoonID, WellID: b.WellID, Resource: b.Resource, Free: b.Free}
|
||||||
|
|||||||
+62
-67
@@ -85,11 +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) | `drill=` follows the copy upgraded |
|
| `upgrade` | `tool` (tool id), `track` (`yield` or `cycle`) | one level on that track; `drill=` follows the copy upgraded |
|
||||||
| `sell-tool` | `tool` | idle, nothing to claim, not the last tool (`noSale`) |
|
| `sell-tool` | `tool` | idle, nothing to claim, not the last tool (`noSale`) |
|
||||||
| `buy-attachment` | `item` (attachment name) | |
|
|
||||||
| `equip`, `unequip` | `item` (attachment name), `to` (tool id) | `drill=` follows the copy used |
|
|
||||||
| `sell-attachment` | `item` | a spare one |
|
|
||||||
| `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 |
|
||||||
@@ -140,18 +137,18 @@ session; the web page also reads it once per page load.
|
|||||||
| `ready`, `idle`, `nextEnd` | finished drills (all moons), idle tools, soonest drill end |
|
| `ready`, `idle`, `nextEnd` | finished drills (all moons), idle tools, soonest drill end |
|
||||||
| `pastPage` | `{page, pages, total, perPage}`: the page of past moons in `past` (`total` in all); ask for others with `page=` |
|
| `pastPage` | `{page, pages, total, perPage}`: the page of past moons in `past` (`total` in all); ask for others with `page=` |
|
||||||
| `moons`, `past` | `past` is one page (`pastPage`). `[{id, name, kind, grade, maxSupply, rating?, wells: [{id, resource, supply}], drills, ready, over, selected, createdAt, finished, finishedAt, mined?, finishedOn?}]`; `rating` is `{tier, label, stars, pct, top}` as on the reveal card (`label` e.g. "Great", `top` the web's "top N%"); `finishedOn` the web's date of a past moon ("Oct 18"); `grade` its moon grade, `maxSupply` its grade's biggest well (a full supply bar) |
|
| `moons`, `past` | `past` is one page (`pastPage`). `[{id, name, kind, grade, maxSupply, rating?, wells: [{id, resource, supply}], drills, ready, over, selected, createdAt, finished, finishedAt, mined?, finishedOn?}]`; `rating` is `{tier, label, stars, pct, top}` as on the reveal card (`label` e.g. "Great", `top` the web's "top N%"); `finishedOn` the web's date of a past moon ("Oct 18"); `grade` its moon grade, `maxSupply` its grade's biggest well (a full supply bar) |
|
||||||
| `tools` | every tool: `{id, storeItemId, name, code, image, tier, maxTier, reach, cycleSecs, yield, attachments, stakeId?}` (`stakeId` set while mining; `code` e.g. `"PD-1"`; `image` a static URL, the tool's art at its tier (`shovel-t2.png` at T2; tier 0 and tiers without art use the base image); `cycleSecs`/`yield` with advisors, its attachments and "all" live events) |
|
| `tools` | every tool: `{id, storeItemId, name, code, image, yieldLevel, cycleLevel, reach, cycleSecs, yield, stakeId?}` (`stakeId` set while mining; `code` e.g. `"PD-1"`; `image` a static URL; `yieldLevel`/`cycleLevel` its levels on its two upgrade tracks (0: the base tool); `cycleSecs`/`yield` at those levels with advisors and "all" live events; `cycleSecs` is in tenths of a second, e.g. `2.6`) |
|
||||||
| `stakes` | every running or unclaimed drill: `{id, moonId, wellId, toolId, resource, amount, reach, reachText, start, end, durationSecs, ready, take, covers?}`; `reachText` the web's "Mines 3 wells" / "Strips the whole moon" (`""` for one well) |
|
| `stakes` | every running or unclaimed drill: `{id, moonId, wellId, toolId, resource, amount, reach, reachText, start, end, durationSecs, ready, take, covers?}`; `reachText` the web's "Mines 3 wells" / "Strips the whole moon" (`""` for one well) |
|
||||||
| `moon` | selected moon (`s=moons`): `{id, name, kind, description, rating, finished, finishedOn?, ready, nextEnd, wells, wide}`; each well `{id, resource, supply, drills, ready, readyAmount, nextEnd, cap, label, wide, selected, redrill, demand, remain, dryAfter, dryAt}`; `drills` are stakes with the claim projection `gets`, `here`, `state` (`ok`, `short`, `none`) and `fate`, the web's tooltip line ("Prospector Drone PD-1 T0: +2 of 3, ready"); `label` is the web's compact name of the well's drills ("PD-1 T1", "PD-1 ×3 +1"); `over` (drills want more than it holds), `full` (they'll drain it), `fullyCommitted` (`remain` is 0 with supply left: a one-well drill can't be deployed on it), `statusText` the well note's lead ("Over-drilled — next claims will come up short", "Fully committed", `""`) and `noteText` the rest ("47 left · 2 committed · 45 free after", "… · runs dry after 3 claims", `""` without demand); `wide` (moon) is the group of multi-well drills (with its own `label`), claimed with `claim-wide`; `sale` is the resale offer (below), once `unlocked.store` |
|
| `moon` | selected moon (`s=moons`): `{id, name, kind, description, rating, finished, finishedOn?, ready, nextEnd, wells, wide}`; each well `{id, resource, supply, drills, ready, readyAmount, nextEnd, cap, label, wide, selected, redrill, demand, remain, dryAfter, dryAt}`; `drills` are stakes with the claim projection `gets`, `here`, `state` (`ok`, `short`, `none`) and `fate`, the web's tooltip line ("Prospector Drone PD-1: +2 of 3, ready"; a tool with upgrades has its levels after the name, "Prospector Drone PD-1 Y1 C2: …"); `label` is the web's compact name of the well's drills ("PD-1", "PD-1 Y1 C2", "PD-1 ×3 +1"); `over` (drills want more than it holds), `full` (they'll drain it), `fullyCommitted` (`remain` is 0 with supply left: a one-well drill can't be deployed on it), `statusText` the well note's lead ("Over-drilled — next claims will come up short", "Fully committed", `""`) and `noteText` the rest ("47 left · 2 committed · 45 free after", "… · runs dry after 3 claims", `""` without demand); `wide` (moon) is the group of multi-well drills (with its own `label`), claimed with `claim-wide`; `sale` is the resale offer (below), once `unlocked.store` |
|
||||||
| `well` | selected well: `{id, resource, rate, idle: [{toolId, storeItemId, name, tier, count, cycleSecs, yield, reach, here, free, short, blocked, attachments}], better: {moonId, wellId, resource, free, toolId}?}` |
|
| `well` | selected well: `{id, resource, rate, idle: [{toolId, storeItemId, name, yieldLevel, cycleLevel, count, cycleSecs, yield, reach, here, free, short, blocked}], better: {moonId, wellId, resource, free, toolId}?}` |
|
||||||
| `market` | once unlocked: `{quotes: [{item, refined, balance, price, rate, boost, advisor, pressure, factor, unit, open, change, high, low, value, symbol, unpressed, base, sellAll, spark?, up, changeText, pressed, pressureText, boosted, boostText, advised, advisorText}], selected, total, held, best, worst, chart}`; `price` is the shared quote (`rate` × `boost`, the same for every player; ticker, cards, `value` = balance × price, `total`); personal, on the payout only: `advisor` (sell bonus), `pressure` and its `factor` (the player's recent sales of the item), `unit` = price × advisor × factor (what one unit pays them now), `unpressed` = price × advisor, `sellAll` what selling the whole balance pays them (a sale of n pays n × unpressed × the pressure factor averaged over the n × base it adds, see `rules.pressure`); the web's texts: `changeText` "+1.25%"/"−0.50%" (`up`: change ≥ 0), `boostText` "×2" when `boosted` (live event, on the quote), and the ticket's personal lines `advisorText` "+8%" when `advised` ("+8% for you") and `pressureText` "−4%" when `pressed` ("−4% from your recent selling") (each `""` otherwise); `chart` and `spark` only for `s=market` (or `include=market`) |
|
| `market` | once unlocked: `{quotes: [{item, refined, balance, price, rate, boost, advisor, pressure, factor, unit, open, change, high, low, value, symbol, unpressed, base, sellAll, spark?, up, changeText, pressed, pressureText, boosted, boostText, advised, advisorText}], selected, total, held, best, worst, chart}`; `price` is the shared quote (`rate` × `boost`, the same for every player; ticker, cards, `value` = balance × price, `total`); personal, on the payout only: `advisor` (sell bonus), `pressure` and its `factor` (the player's recent sales of the item), `unit` = price × advisor × factor (what one unit pays them now), `unpressed` = price × advisor, `sellAll` what selling the whole balance pays them (a sale of n pays n × unpressed × the pressure factor averaged over the n × base it adds, see `rules.pressure`); the web's texts: `changeText` "+1.25%"/"−0.50%" (`up`: change ≥ 0), `boostText` "×2" when `boosted` (live event, on the quote), and the ticket's personal lines `advisorText` "+8%" when `advised` ("+8% for you") and `pressureText` "−4%" when `pressed` ("−4% from your recent selling") (each `""` otherwise); `chart` and `spark` only for `s=market` (or `include=market`) |
|
||||||
| `banner` | `[{level, text, left, kind, id, end}]` announcements, then live events: `kind` `announcement` or `event`, `id` the announcement's or live event's id (an event's matches `liveEvents[].id`), `end` when it ends; `left` the web's "3h 20m left" at `now` (events only, else `""`) |
|
| `banner` | `[{level, text, left, kind, id, end}]` announcements, then live events: `kind` `announcement` or `event`, `id` the announcement's or live event's id (an event's matches `liveEvents[].id`), `end` when it ends; `left` the web's "3h 20m left" at `now` (events only, else `""`) |
|
||||||
| `liveEvents` | active live events: `[{id, name, kind, resource, multiplier, start, end, headline}]`, `kind` `price_multiplier`, `cycle_multiplier` or `yield_multiplier` |
|
| `liveEvents` | active live events: `[{id, name, kind, resource, multiplier, start, end, headline}]`, `kind` `price_multiplier`, `cycle_multiplier` or `yield_multiplier` |
|
||||||
| `showNav` | the section tabs show (the web hides them while Moons would be the only one) |
|
| `showNav` | the section tabs show (the web hides them while Moons would be the only one) |
|
||||||
| `jobs` | once the refinery is open: every batch `[{id, slot, item, qty, start, end, ready}]` (the Refinery tab's badge) |
|
| `jobs` | once the refinery is open: every batch `[{id, slot, item, qty, start, end, ready}]` (the Refinery tab's badge) |
|
||||||
| `bonuses` | active advisor bonuses (the web's bonus chips): `[{kind, mult, capped, label, text}]`, `kind` `speed`, `yield`, `sell`, `refine`; `label` and `text` the chip ("Drill speed", "+10%") |
|
| `bonuses` | active advisor bonuses (the web's bonus chips): `[{kind, mult, capped, label, text}]`, `kind` `speed`, `yield`, `sell`, `refine`; `label` and `text` the chip ("Drill speed", "+10%") |
|
||||||
| `rules` | numbers for prediction: `{wellSupplyMax, maxSupply, slotsPerTool, wellsMin, wellsMax, pressure: {floor, scale, halfLifeSecs, cap}, refinery: {bulkLog, maxBatch, speed}, moonResale: {factor, maxRatio, minHoldSecs}, equipmentResale, bonusCaps: {speed, yield, sell, refine}, bonus: {speed, yield, sell, refine}, boosts: {price, cycle, yield}}`; `maxSupply` is the scale of the web's supply bars (the selected moon's grade max, or its biggest well if bigger; with no moon selected the default grade's); `wellSupplyMax`, `wellsMin`, `wellsMax` are the default grade's (each moon carries its own `maxSupply`); `boosts` are live event multipliers by resource or `"all"` |
|
| `rules` | numbers for prediction: `{wellSupplyMax, maxSupply, wellsMin, wellsMax, pressure: {floor, scale, halfLifeSecs, cap}, refinery: {bulkLog, maxBatch, speed}, moonResale: {factor, maxRatio, minHoldSecs}, equipmentResale, bonusCaps: {speed, yield, sell, refine}, bonus: {speed, yield, sell, refine}, boosts: {price, cycle, yield}}`; `maxSupply` is the scale of the web's supply bars (the selected moon's grade max, or its biggest well if bigger; with no moon selected the default grade's); `wellSupplyMax`, `wellsMin`, `wellsMax` are the default grade's (each moon carries its own `maxSupply`); `boosts` are live event multipliers by resource or `"all"` |
|
||||||
| `catalog` | static data: `{tools: [{storeItemId, name, code, image, description, price, yield, cycleSecs, reach, tiers: [{tier, price, claimBoost, speed, cycleSecs, reach, parts: [{name, amount}], image}]}], attachments: [{name, image, description, price, resale, effect: {speed, yield}, effectText}], advisors: [{name, title, image, description, price, effect: {speed, yield, sell, refine}, effectText}], resources: [{name, color, price, symbol}], refined: [{name, tier, color, price, seconds, inputs, description, symbol}], reach: [{reach, label, short, wells}]}`; images are URLs on this server (`/static/assets/...`; a tool's `image` is its tier 0 art, each tier's `image` its art at that tier); `effectText` e.g. "+20% speed"; `reach` the web's words for a reach from 1 to the whole moon (`label` "Mines 3 wells"/"Strips the whole moon" on cards, `short` "3 wells"/"whole moon" on deploy cards, `wells` "1 well"/"whole moon" in the tier ladder; `""` where the web shows nothing; a bigger reach reads as the last entry). Left out with `catalog=` (see GET /api/state) |
|
| `catalog` | static data: `{tools: [{storeItemId, name, code, image, description, price, yield, cycleSecs, reach, tracks: [{kind, name, levels: [{level, value, price, parts: [{name, amount}]}]}]}], advisors: [{name, title, image, description, price, effect: {speed, yield, sell, refine}, effectText}], resources: [{name, color, price, symbol}], refined: [{name, tier, color, price, seconds, inputs, description, symbol}], reach: [{reach, label, short, wells}]}`; images are URLs on this server (`/static/assets/...`); a tool's `tracks` are its yield track then its cycle track, each level's `value` the claim per cycle or the cycle in seconds from that level on, before bonuses (`reach` never changes); an advisor's `effectText` e.g. "+8% sell prices"; `reach` the web's words for a reach from 1 to the whole moon (`label` "Mines 3 wells"/"Strips the whole moon" on cards, `short` "3 wells"/"whole moon" on deploy cards, `wells` "1 well"/"whole moon"; `""` where the web shows nothing; a bigger reach reads as the last entry). Left out with `catalog=` (see GET /api/state) |
|
||||||
| `catalogVersion` | a hash of the catalog, for `catalog=` |
|
| `catalogVersion` | a hash of the catalog, for `catalog=` |
|
||||||
|
|
||||||
Section blocks (`equipment`, `store`, `refinery`, `advisors`, `leaderboard`, `help`,
|
Section blocks (`equipment`, `store`, `refinery`, `advisors`, `leaderboard`, `help`,
|
||||||
@@ -165,43 +162,43 @@ short code, listed with each field, and the web's text next to it.
|
|||||||
### equipment (`s=equipment`)
|
### equipment (`s=equipment`)
|
||||||
|
|
||||||
```
|
```
|
||||||
{tools: [{id, storeItemId, name, code, image, tier, maxTier, count, idle, reach, cycleSecs, yield,
|
{tools: [{id, storeItemId, name, code, image, yieldLevel, cycleLevel, count, idle, reach, cycleSecs, yield,
|
||||||
attachments, slots, freeSlots, selected, stake, next, preview, ladder, resale, noSale, bonus, gear}],
|
selected, stake, tracks, resale, noSale, noSaleText, bonus}],
|
||||||
drill}
|
drill}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `image`: the tool's art at its tier, like `state.tools`.
|
- Copies of a tool (same type and track levels) are one row: `count` copies, `idle` of them
|
||||||
- Copies of a tool (same type, tier and attachments) are one row: `count` copies, `idle`
|
idle; `id` is the copy shown (the selected one, else an idle one), `stake` its drill.
|
||||||
of them idle; `id` is the copy shown (the selected one, else an idle one), `stake` its drill.
|
- `tracks`: the tool's two upgrade tracks, yield then cycle: `{kind, name, level, max, value,
|
||||||
- `next`: the next tier, `null` at the max: `{tier, price, parts: [{name, need, have}],
|
next, ladder}`. `kind` is `yield` (`value`: the claim per cycle) or `cycle` (`value`: seconds
|
||||||
canAfford, blocked, short, blockedText, shortText}`; `blocked` is `credits` or `parts`
|
per cycle); values are a stake's now, with the player's bonuses and live events. Upgrade
|
||||||
(`""` when it can be bought), `short` the credits missing; `blockedText` the button's
|
with `tool=<id>&track=<kind>`.
|
||||||
tooltip ("Need ₥750 more", "Need 2 Dusk Alloy (you have 0). Make it in the Refinery"),
|
- `next`: the next level, `null` at the top: `{level, value, label, price, parts: [{name,
|
||||||
`shortText` the line under it when credits are short.
|
need, have}], canAfford, blocked, short, blockedText, shortText, rateFrom, rateTo, cost,
|
||||||
- `preview`: what the next tier changes, with the player's bonuses now: `{tier, yieldFrom,
|
paybackSecs}`; `label` the track name and the level it buys ("Twin rotors 2"); `blocked`
|
||||||
yieldTo, secsFrom, secsTo, reachFrom, reachTo, rateFrom, rateTo, cost, paybackSecs}`
|
is `credits` or `parts` (`""` when it can be bought), `short` the credits missing;
|
||||||
(`rate` is credits per hour; `paybackSecs` null if it never pays back).
|
`blockedText` why the button is disabled ("Need ₥750 more", "Need 2 Dusk Alloy (you have
|
||||||
- `ladder`: every tier `[{tier, cycleSecs, yield, reach, rate, current, next}]`.
|
0). Make it in the Refinery"), `shortText` the line under it when credits are short;
|
||||||
- `resale`: what it sells for; `noSale`: `only-tool`, `claim-first`, `mining` or `""`,
|
`rateFrom`/`rateTo` credits per hour now and after it, `cost` its price plus the parts at
|
||||||
`noSaleText` its text ("Claim its haul first").
|
sell value, `paybackSecs` null if it never pays back.
|
||||||
- `bonus`: `{speed, yield}` from advisors and its attachments (capped); `gear` the
|
- `ladder`: every level from 0 `[{level, value, rate, price, parts: [{name, need, have}],
|
||||||
attachments `[{name, image, effect}]`.
|
current, next}]` (the other track as it is; `price` 0 for level 0).
|
||||||
- `drill` (with `drill=<tool id>`, once `unlocked.equipment`, else `null`): the drill panel `{toolId, slots: [{n,
|
- `resale`: what it sells for (its price and its levels' prices × `rules.equipmentResale`);
|
||||||
attachment}], spares: [{name, count, cycleSecs, yield, canEquip, why, whyText, price, resale,
|
`noSale`: `only-tool`, `claim-first`, `mining` or `""`, `noSaleText` its text ("Claim its
|
||||||
confirm}], mining, resale, noSale, noSaleText, confirm, idleSame, idleBest, deployTo, ladder,
|
haul first").
|
||||||
gear}`. `attachment` is `""` for an empty slot (unequip with `item=<name>&to=<toolId>`); a
|
- `bonus`: `{speed, yield}` from advisors (capped).
|
||||||
spare's `cycleSecs`/`yield` are the drill's with it equipped, `why` is `fitted` (one is on
|
- `drill` (with `drill=<tool id>`, once `unlocked.equipment`, else `null`): the drill panel
|
||||||
already) or `full`; `confirm` (panel) is the sell-tool dialog, a spare's the
|
`{toolId, mining, resale, noSale, noSaleText, confirm, idleSame, idleBest, deployTo}`; its
|
||||||
sell-attachment one. `gear[].effectText` e.g. "+20% speed". While it mines, `idleSame` idle tools of its type, `idleBest` the one to deploy
|
tracks and ladders are the selected row's (`selected`). `confirm` is the sell-tool dialog.
|
||||||
and `deployTo` its best well (a suggestion, as in `buyModal`). Its upgrade, slots, spares
|
While it mines, `idleSame` idle tools of its type, `idleBest` the one to deploy and
|
||||||
and Sell drill are Store features (`upgrade`, `equip`, `unequip`, `sell-tool` are refused
|
`deployTo` its best well (a suggestion, as in `buyModal`). Its upgrade buttons and Sell
|
||||||
before it): show them once `unlocked.store`; before that the panel is its stats and `ladder`.
|
drill are Store features (`upgrade` and `sell-tool` are refused before it): show them once
|
||||||
|
`unlocked.store`; before that the panel is its stats and the ladders.
|
||||||
|
|
||||||
### store (`s=store`)
|
### store (`s=store`)
|
||||||
|
|
||||||
```
|
```
|
||||||
{tools: [{storeItemId, name, code, image, description, price, canAfford, owned, yield, cycleSecs, reach, rate, bestRate, whyText}],
|
{tools: [{storeItemId, name, code, image, description, price, canAfford, owned, yield, cycleSecs, reach, rate, bestRate, whyText}]}
|
||||||
attachments: [{name, image, description, effect: {speed, yield}, effectText, price, canAfford, spare, equipped, resale, whyText}]}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`whyText`: the disabled Buy button's tooltip ("Need ₥1,400 more"), `""` when affordable.
|
`whyText`: the disabled Buy button's tooltip ("Need ₥1,400 more"), `""` when affordable.
|
||||||
@@ -324,8 +321,8 @@ is now). Each quote's `spark` is every 3rd point plus the last (the web's sparkl
|
|||||||
|
|
||||||
Confirm dialogs (`confirm`, `buySlot`, `buySeat`, `dismiss`, `newGame`): `{title, text, ok, danger}`,
|
Confirm dialogs (`confirm`, `buySlot`, `buySeat`, `dismiss`, `newGame`): `{title, text, ok, danger}`,
|
||||||
the web's title, question, OK button and whether it's destructive, e.g.
|
the web's title, question, OK button and whether it's destructive, e.g.
|
||||||
`{"title": "Sell drill", "text": "Sell this Prospector Drone PD-1 (T0) for ₥750? Its
|
`{"title": "Sell drill", "text": "Sell this Prospector Drone PD-1 Y1 C2 for ₥980?", "ok":
|
||||||
attachments go back to your inventory.", "ok": "Sell for ₥750", "danger": true}`. Texts with
|
"Sell for ₥980", "danger": true}`. Texts with
|
||||||
a time in them (`whyText` of a held moon, a banner's `left`) are as of `state.now`.
|
a time in them (`whyText` of a held moon, a banner's `left`) are as of `state.now`.
|
||||||
|
|
||||||
Not sent (the client words them from the numbers, like the web's templates):
|
Not sent (the client words them from the numbers, like the web's templates):
|
||||||
@@ -333,12 +330,13 @@ Not sent (the client words them from the numbers, like the web's templates):
|
|||||||
supply" (`over`).
|
supply" (`over`).
|
||||||
- deploy card: "Only X left after current drills", "Well spoken for: nothing left after
|
- deploy card: "Only X left after current drills", "Well spoken for: nothing left after
|
||||||
current drills" (`well.idle[].short`, `here`, `free`).
|
current drills" (`well.idle[].short`, `here`, `free`).
|
||||||
- upgrade payback ("pays back in 2h 5m"), "Max tier", empty states ("No idle tools.").
|
- upgrade payback ("pays back in 2h 5m"), "Max level", empty states ("No idle tools.").
|
||||||
|
- a tool's levels after its name, " Y2 C1" (none for a base tool).
|
||||||
|
|
||||||
The web's formats:
|
The web's formats:
|
||||||
- credits: `₥` + the amount with thousands commas and up to 2 decimals (`₥1,234.5`);
|
- credits: `₥` + the amount with thousands commas and up to 2 decimals (`₥1,234.5`);
|
||||||
amounts the same without `₥`. Compact (per hour): `₥1.2M`, `₥45k`, `₥1.5k`, else whole.
|
amounts the same without `₥`. Compact (per hour): `₥1.2M`, `₥45k`, `₥1.5k`, else whole.
|
||||||
- cycles: `3s`, `2m`, `2m 30s`; longer spans `2h 5m`. Countdowns `m:ss`, `h:mm:ss`.
|
- cycles: `3s`, `2.6s` (tenths under a minute), `2m`, `2m 30s`; longer spans `2h 5m`. Countdowns `m:ss`, `h:mm:ss`.
|
||||||
- banner time left: `2d 4h left`, `3h 20m left`, `45m left`.
|
- banner time left: `2d 4h left`, `3h 20m left`, `45m left`.
|
||||||
- bonuses: `+` and the percent with one decimal at most (`+10%`, `+12.5%`).
|
- bonuses: `+` and the percent with one decimal at most (`+10%`, `+12.5%`).
|
||||||
|
|
||||||
@@ -412,16 +410,16 @@ Content-Type: application/json
|
|||||||
"past": [],
|
"past": [],
|
||||||
"pastPage": {"page": 1, "pages": 1, "total": 0, "perPage": 10},
|
"pastPage": {"page": 1, "pages": 1, "total": 0, "perPage": 10},
|
||||||
"tools": [{"id": "0fbfcb4f-…", "storeItemId": 1, "name": "Prospector Drone PD-1", "code": "PD-1",
|
"tools": [{"id": "0fbfcb4f-…", "storeItemId": 1, "name": "Prospector Drone PD-1", "code": "PD-1",
|
||||||
"image": "/static/assets/items/shovel.png", "tier": 0, "maxTier": 5,
|
"image": "/static/assets/items/shovel.png", "yieldLevel": 0, "cycleLevel": 0,
|
||||||
"reach": 1, "cycleSecs": 4, "yield": 2, "attachments": [], "stakeId": "efc5071d-…"}],
|
"reach": 1, "cycleSecs": 3, "yield": 4, "stakeId": "efc5071d-…"}],
|
||||||
"stakes": [{"id": "efc5071d-…", "moonId": "54daa782-…", "wellId": "b9d643c4-…", "toolId": "0fbfcb4f-…",
|
"stakes": [{"id": "efc5071d-…", "moonId": "54daa782-…", "wellId": "b9d643c4-…", "toolId": "0fbfcb4f-…",
|
||||||
"resource": "Nebulance", "amount": 2, "reach": 1, "start": 1792391815259, "end": 1792391819259,
|
"resource": "Nebulance", "amount": 2, "reach": 1, "start": 1792391815259, "end": 1792391819259,
|
||||||
"durationSecs": 4, "ready": false, "take": 2}],
|
"durationSecs": 4, "ready": false, "take": 2}],
|
||||||
"moon": {"id": "54daa782-…", "name": "Acheris XII", "kind": "Plutargos", "description": "A bleached mauve satellite …",
|
"moon": {"id": "54daa782-…", "name": "Acheris XII", "kind": "Plutargos", "description": "A bleached mauve satellite …",
|
||||||
"rating": {"tier": "fair", "label": "Fair", "stars": 2, "pct": 30, "top": 70}, "finished": false, "ready": 0, "nextEnd": 1792391819259,
|
"rating": {"tier": "fair", "label": "Fair", "stars": 2, "pct": 30, "top": 70}, "finished": false, "ready": 0, "nextEnd": 1792391819259,
|
||||||
"wells": [{"id": "b9d643c4-…", "resource": "Nebulance", "supply": 47,
|
"wells": [{"id": "b9d643c4-…", "resource": "Nebulance", "supply": 47,
|
||||||
"drills": [{"id": "efc5071d-…", "…": "…", "gets": 2, "here": 2, "state": "ok", "fate": "Prospector Drone PD-1 T0: +2"}],
|
"drills": [{"id": "efc5071d-…", "…": "…", "gets": 2, "here": 2, "state": "ok", "fate": "Prospector Drone PD-1: +2"}],
|
||||||
"ready": 0, "readyAmount": 0, "nextEnd": 1792391819259, "cap": 47, "label": "PD-1 T0", "wide": [],
|
"ready": 0, "readyAmount": 0, "nextEnd": 1792391819259, "cap": 47, "label": "PD-1", "wide": [],
|
||||||
"selected": true, "redrill": 0, "demand": 2, "remain": 45, "dryAfter": 0, "dryAt": null}],
|
"selected": true, "redrill": 0, "demand": 2, "remain": 45, "dryAfter": 0, "dryAt": null}],
|
||||||
"wide": null},
|
"wide": null},
|
||||||
"well": {"id": "b9d643c4-…", "resource": "Nebulance", "rate": 11.157, "idle": [], "better": null},
|
"well": {"id": "b9d643c4-…", "resource": "Nebulance", "rate": 11.157, "idle": [], "better": null},
|
||||||
@@ -431,8 +429,8 @@ Content-Type: application/json
|
|||||||
"selected": "Azurium", "total": 0},
|
"selected": "Azurium", "total": 0},
|
||||||
"banner": [],
|
"banner": [],
|
||||||
"showNav": true, "jobs": [], "bonuses": [], "liveEvents": [],
|
"showNav": true, "jobs": [], "bonuses": [], "liveEvents": [],
|
||||||
"rules": {"wellSupplyMax": 90, "maxSupply": 90, "slotsPerTool": 2, "…": "…"},
|
"rules": {"wellSupplyMax": 90, "maxSupply": 90, "…": "…"},
|
||||||
"catalog": {"tools": ["…"], "attachments": ["…"], "advisors": ["…"], "resources": ["…"], "refined": ["…"], "reach": ["…"]},
|
"catalog": {"tools": ["…"], "advisors": ["…"], "resources": ["…"], "refined": ["…"], "reach": ["…"]},
|
||||||
"catalogVersion": "9adb4eabab929f5d"
|
"catalogVersion": "9adb4eabab929f5d"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -445,20 +443,20 @@ From a live server (long ids and arrays shortened).
|
|||||||
`GET /api/state?s=equipment&drill=7e40…`, `state.equipment`:
|
`GET /api/state?s=equipment&drill=7e40…`, `state.equipment`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"tools": [{"id": "7e40…", "storeItemId": 1, "name": "Prospector Drone PD-1", "code": "PD-1",
|
{"tools": [{"id": "13b3…", "storeItemId": 1, "name": "Prospector Drone PD-1", "code": "PD-1",
|
||||||
"image": "/static/assets/items/shovel-t1.png", "tier": 1, "maxTier": 5, "count": 1, "idle": 1, "reach": 1,
|
"image": "/static/assets/items/shovel.png", "yieldLevel": 1, "cycleLevel": 2, "count": 1, "idle": 1,
|
||||||
"cycleSecs": 3, "yield": 3, "attachments": ["Turbo Core"], "slots": 2, "freeSlots": 1, "selected": true,
|
"reach": 1, "cycleSecs": 2.2, "yield": 5, "selected": true, "stake": null,
|
||||||
"stake": null, "next": {"tier": 2, "price": 900, "parts": [], "canAfford": true, "blocked": "", "short": 0},
|
"tracks": [{"kind": "yield", "name": "Twin rotors", "level": 1, "max": 4, "value": 5,
|
||||||
"preview": {"tier": 2, "yieldFrom": 3, "yieldTo": 4, "secsFrom": 3, "secsTo": 3, "reachFrom": 1, "reachTo": 1,
|
"next": {"level": 2, "value": 6, "label": "Twin rotors 2", "price": 320, "parts": [],
|
||||||
"rateFrom": 24015.57, "rateTo": 32020.76, "cost": 900, "paybackSecs": 404.7},
|
"canAfford": true, "blocked": "", "short": 0, "blockedText": "", "shortText": "",
|
||||||
"ladder": [{"tier": 0, "cycleSecs": 3, "yield": 2, "reach": 1, "rate": 16010.38, "current": false, "next": false}, "…"],
|
"rateFrom": 101570.42, "rateTo": 121884.5, "cost": 320, "paybackSecs": 56.7},
|
||||||
"resale": 1175, "noSale": "", "bonus": {"speed": 1.2, "yield": 1},
|
"ladder": [{"level": 0, "value": 4, "rate": 81256.33, "price": 0, "parts": [], "current": false, "next": false},
|
||||||
"gear": [{"name": "Turbo Core", "image": "/static/assets/attachments/turbo-core.png", "effect": {"speed": 1.2, "yield": 1}}]}],
|
{"level": 1, "value": 5, "rate": 101570.42, "price": 290, "parts": [], "current": true, "next": false}, "…"]},
|
||||||
"drill": {"toolId": "7e40…", "slots": [{"n": 1, "attachment": "Turbo Core"}, {"n": 2, "attachment": ""}],
|
{"kind": "cycle", "name": "Flight computer", "level": 2, "max": 5, "value": 2.2, "next": "…", "ladder": ["…"]}],
|
||||||
"spares": [{"name": "Diamond Bit", "count": 1, "cycleSecs": 3, "yield": 2.3, "canEquip": true, "why": "",
|
"resale": 980, "noSale": "", "noSaleText": "", "bonus": {"speed": 1, "yield": 1}}],
|
||||||
"price": 4000, "resale": 2000}],
|
"drill": {"toolId": "13b3…", "mining": false, "resale": 980, "noSale": "", "noSaleText": "",
|
||||||
"mining": false, "resale": 750, "noSale": "", "idleSame": 0, "idleBest": "", "deployTo": null,
|
"confirm": {"title": "Sell drill", "text": "Sell this Prospector Drone PD-1 Y1 C2 for ₥980?", "ok": "Sell for ₥980", "danger": true},
|
||||||
"ladder": ["…"], "gear": ["…"]}}
|
"idleSame": 0, "idleBest": "", "deployTo": null}}
|
||||||
```
|
```
|
||||||
|
|
||||||
`s=store`, `state.store`:
|
`s=store`, `state.store`:
|
||||||
@@ -466,10 +464,7 @@ From a live server (long ids and arrays shortened).
|
|||||||
```json
|
```json
|
||||||
{"tools": [{"storeItemId": 1, "name": "Prospector Drone PD-1", "code": "PD-1", "image": "/static/assets/items/shovel.png",
|
{"tools": [{"storeItemId": 1, "name": "Prospector Drone PD-1", "code": "PD-1", "image": "/static/assets/items/shovel.png",
|
||||||
"description": "A hovering scout drone that skims ore off the surface.", "price": 1500, "canAfford": true,
|
"description": "A hovering scout drone that skims ore off the surface.", "price": 1500, "canAfford": true,
|
||||||
"owned": 1, "yield": 2, "cycleSecs": 4, "reach": 1, "rate": 12007.78, "bestRate": 12007.78}, "…"],
|
"owned": 1, "yield": 4, "cycleSecs": 3, "reach": 1, "rate": 12007.78, "bestRate": 12007.78}, "…"]}
|
||||||
"attachments": [{"name": "Turbo Core", "image": "/static/assets/attachments/turbo-core.png",
|
|
||||||
"description": "An overdriven motor core. Runs hot, cycles fast.", "effect": {"speed": 1.2, "yield": 1},
|
|
||||||
"price": 3000, "canAfford": true, "spare": 0, "equipped": 0, "resale": 1500}, "…"]}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`s=refinery`, `state.refinery`:
|
`s=refinery`, `state.refinery`:
|
||||||
|
|||||||
+105
-195
@@ -10,23 +10,20 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Image URLs, as the templates build them.
|
// Image URLs, as the templates build them.
|
||||||
const (
|
const (
|
||||||
itemImages = "/static/assets/items/"
|
itemImages = "/static/assets/items/"
|
||||||
attachmentImages = "/static/assets/attachments/"
|
|
||||||
advisorImages = "/static/assets/advisors/"
|
advisorImages = "/static/assets/advisors/"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ------------------------------------------------------------------ always present
|
// ------------------------------------------------------------------ always present
|
||||||
|
|
||||||
// APICatalog is the static game data: every tool, attachment, advisor, resource and
|
// APICatalog is the static game data: every tool (with its upgrade tracks), advisor,
|
||||||
// refined item with its image and color.
|
// resource and refined item with its image and color.
|
||||||
type APICatalog struct {
|
type APICatalog struct {
|
||||||
Tools []APICatalogTool `json:"tools"`
|
Tools []APICatalogTool `json:"tools"`
|
||||||
Attachments []APICatalogAttachment `json:"attachments"`
|
|
||||||
Advisors []APICatalogAdvisor `json:"advisors"`
|
Advisors []APICatalogAdvisor `json:"advisors"`
|
||||||
Resources []APICatalogResource `json:"resources"`
|
Resources []APICatalogResource `json:"resources"`
|
||||||
Refined []APICatalogRefined `json:"refined"`
|
Refined []APICatalogRefined `json:"refined"`
|
||||||
@@ -35,7 +32,7 @@ type APICatalog struct {
|
|||||||
|
|
||||||
// APIReachText is how the web words a tool's reach (wells one cycle mines): label on cards
|
// APIReachText is how the web words a tool's reach (wells one cycle mines): label on cards
|
||||||
// ("Mines 3 wells", "Strips the whole moon"; "" for one), short on deploy cards ("3 wells",
|
// ("Mines 3 wells", "Strips the whole moon"; "" for one), short on deploy cards ("3 wells",
|
||||||
// "whole moon"; "" for one), wells in the tier ladder ("1 well", "whole moon"). A reach at or
|
// "whole moon"; "" for one), wells ("1 well", "whole moon"). A reach at or
|
||||||
// above the last entry's is the whole moon.
|
// above the last entry's is the whole moon.
|
||||||
type APIReachText struct {
|
type APIReachText struct {
|
||||||
Reach int `json:"reach"`
|
Reach int `json:"reach"`
|
||||||
@@ -51,21 +48,25 @@ type APICatalogTool struct {
|
|||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
Yield float64 `json:"yield"` // base, tier 0
|
Yield float64 `json:"yield"` // base (level 0)
|
||||||
CycleSecs int `json:"cycleSecs"` // base
|
CycleSecs float64 `json:"cycleSecs"` // base
|
||||||
Reach int `json:"reach"` // tier 0
|
Reach int `json:"reach"`
|
||||||
Tiers []APITierUp `json:"tiers"` // upgrades, tier 1 on
|
Tracks []APICatalogTrack `json:"tracks"` // yield, then cycle
|
||||||
}
|
}
|
||||||
|
|
||||||
type APITierUp struct {
|
// APICatalogTrack is one of a tool's upgrade tracks: its levels from 1, each setting the
|
||||||
Tier int `json:"tier"`
|
// track's value (yield per cycle, or cycle seconds) before bonuses.
|
||||||
|
type APICatalogTrack struct {
|
||||||
|
Kind string `json:"kind"` // yield, cycle (upgrade with track=<kind>)
|
||||||
|
Name string `json:"name"`
|
||||||
|
Levels []APITrackLevel `json:"levels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APITrackLevel struct {
|
||||||
|
Level int `json:"level"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
ClaimBoost float64 `json:"claimBoost"`
|
|
||||||
Speed float64 `json:"speed"` // cycle speed multiplier at this tier (1: none)
|
|
||||||
CycleSecs int `json:"cycleSecs"` // the tier's cycle before bonuses
|
|
||||||
Reach int `json:"reach"` // wells per cycle from this tier on
|
|
||||||
Parts []APIAmount `json:"parts"` // refined items it uses up
|
Parts []APIAmount `json:"parts"` // refined items it uses up
|
||||||
Image string `json:"image"` // the tool's image at this tier
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIEffect struct {
|
type APIEffect struct {
|
||||||
@@ -75,16 +76,6 @@ type APIEffect struct {
|
|||||||
Refine float64 `json:"refine,omitempty"`
|
Refine float64 `json:"refine,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type APICatalogAttachment struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Image string `json:"image"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Price float64 `json:"price"`
|
|
||||||
Resale float64 `json:"resale"`
|
|
||||||
Effect APIEffect `json:"effect"`
|
|
||||||
EffectText string `json:"effectText"` // e.g. "+20% speed"
|
|
||||||
}
|
|
||||||
|
|
||||||
type APICatalogAdvisor struct {
|
type APICatalogAdvisor struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@@ -141,27 +132,23 @@ func apiNeed(need map[string]float64) []APIAmount {
|
|||||||
|
|
||||||
func apiCatalog(s *State) *APICatalog {
|
func apiCatalog(s *State) *APICatalog {
|
||||||
c := cfg()
|
c := cfg()
|
||||||
cat := &APICatalog{Tools: []APICatalogTool{}, Attachments: []APICatalogAttachment{}, Advisors: []APICatalogAdvisor{},
|
cat := &APICatalog{Tools: []APICatalogTool{}, Advisors: []APICatalogAdvisor{},
|
||||||
Resources: []APICatalogResource{}, Refined: []APICatalogRefined{}, Reach: []APIReachText{}}
|
Resources: []APICatalogResource{}, Refined: []APICatalogRefined{}, Reach: []APIReachText{}}
|
||||||
for i := range s.Config.StoreItems {
|
for i := range s.Config.StoreItems {
|
||||||
item := &s.Config.StoreItems[i]
|
item := &s.Config.StoreItems[i]
|
||||||
t := APICatalogTool{StoreItemID: item.ID, Name: item.Name, Code: toolCode(item.Name), Image: itemImages + item.Image,
|
t := APICatalogTool{StoreItemID: item.ID, Name: item.Name, Code: toolCode(item.Name), Image: itemImages + item.Image,
|
||||||
Description: item.Description, Price: item.Price, Yield: item.ClaimAmount, CycleSecs: item.CompletionTimeInSecs,
|
Description: item.Description, Price: item.Price, Yield: item.ClaimAmount,
|
||||||
Reach: max(1, item.ToolReach(0)), Tiers: []APITierUp{}}
|
CycleSecs: float64(item.CompletionTimeInSecs), Reach: item.ToolReach(), Tracks: []APICatalogTrack{}}
|
||||||
for _, u := range item.Upgrades {
|
for _, kind := range trackKinds {
|
||||||
t.Tiers = append(t.Tiers, APITierUp{Tier: u.Tier, Price: u.Price, ClaimBoost: u.ClaimBoost,
|
tr := item.Track(kind)
|
||||||
Speed: max(1, u.Speed), CycleSecs: item.ToolSecs(u.Tier),
|
ct := APICatalogTrack{Kind: kind, Name: tr.Name, Levels: []APITrackLevel{}}
|
||||||
Reach: max(1, item.ToolReach(u.Tier)), Parts: apiNeed(upgradeParts(item.ID, u.Tier))})
|
for n, l := range tr.Levels {
|
||||||
t.Tiers[len(t.Tiers)-1].Image = itemImages + item.ImageAt(u.Tier)
|
ct.Levels = append(ct.Levels, APITrackLevel{Level: n + 1, Value: l.Value, Price: l.Price, Parts: apiNeed(l.Parts)})
|
||||||
|
}
|
||||||
|
t.Tracks = append(t.Tracks, ct)
|
||||||
}
|
}
|
||||||
cat.Tools = append(cat.Tools, t)
|
cat.Tools = append(cat.Tools, t)
|
||||||
}
|
}
|
||||||
for i := range c.Attachments.Items {
|
|
||||||
a := &c.Attachments.Items[i]
|
|
||||||
cat.Attachments = append(cat.Attachments, APICatalogAttachment{Name: a.Name, Image: attachmentImages + a.Image,
|
|
||||||
Description: a.Description, Price: a.Price, Resale: a.Resale(), Effect: effectOf(a.Speed, a.Yield, 0, 0),
|
|
||||||
EffectText: a.Effect()})
|
|
||||||
}
|
|
||||||
for i := range c.Advisors.Items {
|
for i := range c.Advisors.Items {
|
||||||
a := &c.Advisors.Items[i]
|
a := &c.Advisors.Items[i]
|
||||||
cat.Advisors = append(cat.Advisors, APICatalogAdvisor{Name: a.Name, Title: a.Title, Image: advisorImages + a.Image,
|
cat.Advisors = append(cat.Advisors, APICatalogAdvisor{Name: a.Name, Title: a.Title, Image: advisorImages + a.Image,
|
||||||
@@ -210,7 +197,6 @@ type APIConfirm struct {
|
|||||||
type APIRules struct {
|
type APIRules struct {
|
||||||
WellSupplyMax float64 `json:"wellSupplyMax"` // the default grade's biggest well (each moon has its maxSupply)
|
WellSupplyMax float64 `json:"wellSupplyMax"` // the default grade's biggest well (each moon has its maxSupply)
|
||||||
MaxSupply float64 `json:"maxSupply"` // the scale of the web's supply bars: the selected moon's grade
|
MaxSupply float64 `json:"maxSupply"` // the scale of the web's supply bars: the selected moon's grade
|
||||||
SlotsPerTool int `json:"slotsPerTool"`
|
|
||||||
WellsMin int `json:"wellsMin"`
|
WellsMin int `json:"wellsMin"`
|
||||||
WellsMax int `json:"wellsMax"`
|
WellsMax int `json:"wellsMax"`
|
||||||
Pressure APIPressure `json:"pressure"`
|
Pressure APIPressure `json:"pressure"`
|
||||||
@@ -218,7 +204,7 @@ type APIRules struct {
|
|||||||
MoonResale MoonResale `json:"moonResale"`
|
MoonResale MoonResale `json:"moonResale"`
|
||||||
EquipmentResale float64 `json:"equipmentResale"` // factor of the credit price
|
EquipmentResale float64 `json:"equipmentResale"` // factor of the credit price
|
||||||
BonusCaps BonusCaps `json:"bonusCaps"`
|
BonusCaps BonusCaps `json:"bonusCaps"`
|
||||||
Bonus APIEffect `json:"bonus"` // the advisors' bonus now, capped (tools add attachments)
|
Bonus APIEffect `json:"bonus"` // the advisors' bonus now, capped
|
||||||
Boosts map[string]APIMap `json:"boosts"` // live event multipliers: price, cycle, yield, by resource or "all"
|
Boosts map[string]APIMap `json:"boosts"` // live event multipliers: price, cycle, yield, by resource or "all"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +227,7 @@ func apiRules(p *Page) *APIRules {
|
|||||||
s, c := p.S, cfg()
|
s, c := p.S, cfg()
|
||||||
b := s.Bonus()
|
b := s.Bonus()
|
||||||
lo, hi := s.wellCountRange()
|
lo, hi := s.wellCountRange()
|
||||||
r := &APIRules{WellSupplyMax: s.MoonGrade().WellSupplyMax, MaxSupply: p.MaxSupply, SlotsPerTool: c.Attachments.SlotsPerTool,
|
r := &APIRules{WellSupplyMax: s.MoonGrade().WellSupplyMax, MaxSupply: p.MaxSupply,
|
||||||
WellsMin: lo, WellsMax: hi,
|
WellsMin: lo, WellsMax: hi,
|
||||||
Pressure: APIPressure{Floor: c.MarketPressure.Floor, Scale: c.MarketPressure.Scale,
|
Pressure: APIPressure{Floor: c.MarketPressure.Floor, Scale: c.MarketPressure.Scale,
|
||||||
HalfLifeSecs: c.MarketPressure.HalfLifeSecs, Cap: pressureCap * c.MarketPressure.Scale},
|
HalfLifeSecs: c.MarketPressure.HalfLifeSecs, Cap: pressureCap * c.MarketPressure.Scale},
|
||||||
@@ -317,7 +303,7 @@ type APILiveEvent struct {
|
|||||||
// ------------------------------------------------------------------ equipment
|
// ------------------------------------------------------------------ equipment
|
||||||
|
|
||||||
type APIEquipment struct {
|
type APIEquipment struct {
|
||||||
Tools []APIToolRow `json:"tools"` // copies of a tool (same type, tier, attachments) grouped
|
Tools []APIToolRow `json:"tools"` // copies of a tool (same type and levels) grouped
|
||||||
Drill *APIDrillPanel `json:"drill"` // drill=<tool id>
|
Drill *APIDrillPanel `json:"drill"` // drill=<tool id>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,37 +313,38 @@ type APIToolRow struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
Tier int `json:"tier"`
|
YieldLevel int `json:"yieldLevel"`
|
||||||
MaxTier int `json:"maxTier"`
|
CycleLevel int `json:"cycleLevel"`
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
Idle int `json:"idle"`
|
Idle int `json:"idle"`
|
||||||
Reach int `json:"reach"`
|
Reach int `json:"reach"`
|
||||||
CycleSecs int `json:"cycleSecs"`
|
CycleSecs float64 `json:"cycleSecs"`
|
||||||
Yield float64 `json:"yield"`
|
Yield float64 `json:"yield"`
|
||||||
Attachments []string `json:"attachments"`
|
|
||||||
Slots int `json:"slots"`
|
|
||||||
FreeSlots int `json:"freeSlots"`
|
|
||||||
Selected bool `json:"selected"`
|
Selected bool `json:"selected"`
|
||||||
Stake *APIStake `json:"stake"` // the shown copy's drill
|
Stake *APIStake `json:"stake"` // the shown copy's drill
|
||||||
Next *APINextTier `json:"next"` // null at the max tier
|
Tracks []APITrack `json:"tracks"` // yield, then cycle
|
||||||
Preview *APIPreview `json:"preview"`
|
|
||||||
Ladder []APITierRow `json:"ladder"`
|
|
||||||
Resale float64 `json:"resale"`
|
Resale float64 `json:"resale"`
|
||||||
NoSale string `json:"noSale"` // only-tool, claim-first, mining; "" can sell
|
NoSale string `json:"noSale"` // only-tool, claim-first, mining; "" can sell
|
||||||
NoSaleText string `json:"noSaleText"`
|
NoSaleText string `json:"noSaleText"`
|
||||||
Bonus APIEffect `json:"bonus"` // advisors and its attachments, capped
|
Bonus APIEffect `json:"bonus"` // advisors, capped
|
||||||
Gear []APIGearDetail `json:"gear"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIGearDetail struct {
|
// APITrack is one of a tool's upgrade tracks: values are a stake's now (yield per cycle,
|
||||||
|
// or the cycle in seconds), with bonuses and live events.
|
||||||
|
type APITrack struct {
|
||||||
|
Kind string `json:"kind"` // yield, cycle: upgrade with tool=<id>&track=<kind>
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Image string `json:"image"`
|
Level int `json:"level"`
|
||||||
Effect APIEffect `json:"effect"`
|
Max int `json:"max"`
|
||||||
EffectText string `json:"effectText"`
|
Value float64 `json:"value"`
|
||||||
|
Next *APINextLevel `json:"next"` // null at the top level
|
||||||
|
Ladder []APILadderStep `json:"ladder"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type APINextTier struct {
|
type APINextLevel struct {
|
||||||
Tier int `json:"tier"`
|
Level int `json:"level"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
Label string `json:"label"` // the button's track name and level, "Twin rotors 2"
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
Parts []APIPart `json:"parts"`
|
Parts []APIPart `json:"parts"`
|
||||||
CanAfford bool `json:"canAfford"` // credits and parts
|
CanAfford bool `json:"canAfford"` // credits and parts
|
||||||
@@ -367,6 +354,10 @@ type APINextTier struct {
|
|||||||
// when credits are short ("Need ₥X more").
|
// when credits are short ("Need ₥X more").
|
||||||
BlockedText string `json:"blockedText"`
|
BlockedText string `json:"blockedText"`
|
||||||
ShortText string `json:"shortText"`
|
ShortText string `json:"shortText"`
|
||||||
|
RateFrom float64 `json:"rateFrom"` // credits per hour now and after it
|
||||||
|
RateTo float64 `json:"rateTo"`
|
||||||
|
Cost float64 `json:"cost"` // price plus the parts at sell value
|
||||||
|
PaybackSecs *float64 `json:"paybackSecs"` // null if it never pays back
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIPart struct {
|
type APIPart struct {
|
||||||
@@ -375,34 +366,18 @@ type APIPart struct {
|
|||||||
Have float64 `json:"have"`
|
Have float64 `json:"have"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIPreview struct {
|
type APILadderStep struct {
|
||||||
Tier int `json:"tier"`
|
Level int `json:"level"`
|
||||||
YieldFrom float64 `json:"yieldFrom"`
|
Value float64 `json:"value"`
|
||||||
YieldTo float64 `json:"yieldTo"`
|
|
||||||
SecsFrom int `json:"secsFrom"`
|
|
||||||
SecsTo int `json:"secsTo"`
|
|
||||||
ReachFrom int `json:"reachFrom"`
|
|
||||||
ReachTo int `json:"reachTo"`
|
|
||||||
RateFrom float64 `json:"rateFrom"` // credits per hour
|
|
||||||
RateTo float64 `json:"rateTo"`
|
|
||||||
Cost float64 `json:"cost"` // price plus the parts at sell value
|
|
||||||
PaybackSecs *float64 `json:"paybackSecs"` // null if it never pays back
|
|
||||||
}
|
|
||||||
|
|
||||||
type APITierRow struct {
|
|
||||||
Tier int `json:"tier"`
|
|
||||||
CycleSecs int `json:"cycleSecs"`
|
|
||||||
Yield float64 `json:"yield"`
|
|
||||||
Reach int `json:"reach"`
|
|
||||||
Rate float64 `json:"rate"` // credits per hour
|
Rate float64 `json:"rate"` // credits per hour
|
||||||
|
Price float64 `json:"price"` // 0 for level 0
|
||||||
|
Parts []APIPart `json:"parts"`
|
||||||
Current bool `json:"current"`
|
Current bool `json:"current"`
|
||||||
Next bool `json:"next"`
|
Next bool `json:"next"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIDrillPanel struct {
|
type APIDrillPanel struct {
|
||||||
ToolID string `json:"toolId"`
|
ToolID string `json:"toolId"`
|
||||||
Slots []APISlot `json:"slots"`
|
|
||||||
Spares []APISpare `json:"spares"`
|
|
||||||
Mining bool `json:"mining"`
|
Mining bool `json:"mining"`
|
||||||
Resale float64 `json:"resale"`
|
Resale float64 `json:"resale"`
|
||||||
NoSale string `json:"noSale"`
|
NoSale string `json:"noSale"`
|
||||||
@@ -411,112 +386,67 @@ type APIDrillPanel struct {
|
|||||||
IdleSame int `json:"idleSame"` // while it mines: idle tools of its type
|
IdleSame int `json:"idleSame"` // while it mines: idle tools of its type
|
||||||
IdleBest string `json:"idleBest"` // the one pickTool would deploy
|
IdleBest string `json:"idleBest"` // the one pickTool would deploy
|
||||||
DeployTo *APISuggestion `json:"deployTo"` // its best well
|
DeployTo *APISuggestion `json:"deployTo"` // its best well
|
||||||
Ladder []APITierRow `json:"ladder"`
|
|
||||||
Gear []APIGearDetail `json:"gear"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type APISlot struct {
|
func apiParts(rows []PartRow) []APIPart {
|
||||||
N int `json:"n"`
|
out := []APIPart{}
|
||||||
Attachment string `json:"attachment"` // "" empty; unequip with item=<name>&to=<toolId>
|
for _, p := range rows {
|
||||||
|
out = append(out, APIPart{Name: p.Name, Need: p.Need, Have: p.Have})
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
type APISpare struct {
|
func apiTracks(s *State, rows []TrackRow) []APITrack {
|
||||||
Name string `json:"name"`
|
out := []APITrack{}
|
||||||
Count int `json:"count"`
|
|
||||||
CycleSecs int `json:"cycleSecs"` // the drill's stats with it equipped
|
|
||||||
Yield float64 `json:"yield"`
|
|
||||||
CanEquip bool `json:"canEquip"`
|
|
||||||
Why string `json:"why"` // fitted, full
|
|
||||||
WhyText string `json:"whyText"`
|
|
||||||
Price float64 `json:"price"`
|
|
||||||
Resale float64 `json:"resale"`
|
|
||||||
Confirm APIConfirm `json:"confirm"` // before sell-attachment
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiTierRows(rows []TierRow) []APITierRow {
|
|
||||||
out := []APITierRow{}
|
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
out = append(out, APITierRow{Tier: r.Tier, CycleSecs: r.Secs, Yield: r.Yield, Reach: r.Reach, Rate: r.Rate,
|
t := APITrack{Kind: r.Kind, Name: r.Name, Level: r.Level, Max: r.Max, Value: r.Value, Ladder: []APILadderStep{}}
|
||||||
Current: r.Current, Next: r.Next})
|
for _, st := range r.Ladder {
|
||||||
|
t.Ladder = append(t.Ladder, APILadderStep{Level: st.Level, Value: st.Value, Rate: st.Rate, Price: st.Price,
|
||||||
|
Parts: apiParts(st.Parts), Current: st.Current, Next: st.Next})
|
||||||
}
|
}
|
||||||
return out
|
if n := r.Next; n != nil {
|
||||||
}
|
next := &APINextLevel{Level: r.Level + 1, Value: r.NextValue, Label: r.Label(), Price: n.Price,
|
||||||
|
Parts: apiParts(r.Parts), CanAfford: r.CanAfford, Short: max(0, n.Price-s.Balance),
|
||||||
func apiGearDetail(gear []*Attachment) []APIGearDetail {
|
BlockedText: r.Missing, ShortText: r.Short, RateFrom: r.RateFrom, RateTo: r.RateTo, Cost: r.Cost}
|
||||||
out := []APIGearDetail{}
|
for _, part := range r.Parts {
|
||||||
for _, a := range gear {
|
if !part.OK() && next.Blocked == "" {
|
||||||
out = append(out, APIGearDetail{Name: a.Name, Image: attachmentImages + a.Image, Effect: effectOf(a.Speed, a.Yield, 0, 0),
|
|
||||||
EffectText: a.Effect()})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiEquipment(p *Page) *APIEquipment {
|
|
||||||
s := p.S
|
|
||||||
e := &APIEquipment{Tools: []APIToolRow{}}
|
|
||||||
for _, row := range p.Equipment {
|
|
||||||
b := s.toolBonus(row.ID)
|
|
||||||
t := APIToolRow{ID: row.ID, StoreItemID: row.StoreItemID, Name: row.Item.Name, Code: toolCode(row.Item.Name),
|
|
||||||
Image: itemImages + row.Item.ImageAt(row.Tier), Tier: row.Tier, MaxTier: row.MaxTier, Count: row.Count, Idle: row.Idle,
|
|
||||||
Reach: max(1, row.Reach), CycleSecs: row.Secs, Yield: row.Yield, Attachments: apiGear(row.Gear),
|
|
||||||
Slots: len(row.Gear) + row.FreeSlots, FreeSlots: row.FreeSlots, Selected: row.Selected,
|
|
||||||
Ladder: apiTierRows(row.Ladder), Resale: row.Item.ToolResale(row.Tier), NoSale: s.noSale(row),
|
|
||||||
NoSaleText: noSaleText[s.noSale(row)],
|
|
||||||
Bonus: APIEffect{Speed: b.Speed, Yield: b.Yield}, Gear: apiGearDetail(row.Gear)}
|
|
||||||
if row.Stake != nil {
|
|
||||||
st := apiStake(*row.Stake, false)
|
|
||||||
t.Stake = &st
|
|
||||||
}
|
|
||||||
if n := row.Next; n != nil {
|
|
||||||
next := &APINextTier{Tier: n.Tier, Price: n.Price, Parts: []APIPart{}, CanAfford: row.CanAfford,
|
|
||||||
Short: max(0, n.Price-s.Balance), BlockedText: row.Missing, ShortText: row.Short}
|
|
||||||
for _, part := range row.Parts {
|
|
||||||
next.Parts = append(next.Parts, APIPart{Name: part.Name, Need: part.Need, Have: part.Have})
|
|
||||||
if !part.OK() && next.Blocked == "" && next.Short == 0 {
|
|
||||||
next.Blocked = "parts"
|
next.Blocked = "parts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if next.Short > 0 {
|
if next.Short > 0 {
|
||||||
next.Blocked = "credits"
|
next.Blocked = "credits"
|
||||||
}
|
}
|
||||||
|
if gain := r.RateGain(); gain > 0 {
|
||||||
|
secs := r.Cost / gain * 3600
|
||||||
|
next.PaybackSecs = &secs
|
||||||
|
}
|
||||||
t.Next = next
|
t.Next = next
|
||||||
}
|
}
|
||||||
if u := row.Preview; u != nil {
|
out = append(out, t)
|
||||||
pv := &APIPreview{Tier: u.Tier, YieldFrom: u.YieldFrom, YieldTo: u.YieldTo, SecsFrom: u.SecsFrom, SecsTo: u.SecsTo,
|
|
||||||
ReachFrom: max(1, u.ReachFrom), ReachTo: max(1, u.ReachTo), RateFrom: u.RateFrom, RateTo: u.RateTo, Cost: u.Cost}
|
|
||||||
if gain := u.RateGain(); gain > 0 {
|
|
||||||
secs := u.Cost / gain * 3600
|
|
||||||
pv.PaybackSecs = &secs
|
|
||||||
}
|
}
|
||||||
t.Preview = pv
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiEquipment(p *Page) *APIEquipment {
|
||||||
|
s := p.S
|
||||||
|
b := s.Bonus()
|
||||||
|
e := &APIEquipment{Tools: []APIToolRow{}}
|
||||||
|
for _, row := range p.Equipment {
|
||||||
|
t := APIToolRow{ID: row.ID, StoreItemID: row.StoreItemID, Name: row.Item.Name, Code: toolCode(row.Item.Name),
|
||||||
|
Image: itemImages + row.Item.Image, YieldLevel: row.YieldLevel, CycleLevel: row.CycleLevel,
|
||||||
|
Count: row.Count, Idle: row.Idle, Reach: max(1, row.Reach), CycleSecs: row.Secs, Yield: row.Yield,
|
||||||
|
Selected: row.Selected, Tracks: apiTracks(s, row.Tracks), Resale: row.Item.ToolResale(row.Levels()),
|
||||||
|
NoSale: s.noSale(row), NoSaleText: noSaleText[s.noSale(row)], Bonus: APIEffect{Speed: b.Speed, Yield: b.Yield}}
|
||||||
|
if row.Stake != nil {
|
||||||
|
st := apiStake(*row.Stake, false)
|
||||||
|
t.Stake = &st
|
||||||
}
|
}
|
||||||
e.Tools = append(e.Tools, t)
|
e.Tools = append(e.Tools, t)
|
||||||
}
|
}
|
||||||
if d := p.Drill; d != nil {
|
if d := p.Drill; d != nil {
|
||||||
dp := &APIDrillPanel{ToolID: d.ID, Slots: []APISlot{}, Spares: []APISpare{}, Mining: d.Mining, Resale: d.Resale,
|
dp := &APIDrillPanel{ToolID: d.ID, Mining: d.Mining, Resale: d.Resale,
|
||||||
NoSale: s.noSale(d.ToolRow), NoSaleText: d.NoSale, IdleSame: d.IdleSame, Ladder: apiTierRows(d.Ladder),
|
NoSale: s.noSale(d.ToolRow), NoSaleText: d.NoSale, IdleSame: d.IdleSame,
|
||||||
Gear: apiGearDetail(d.Gear),
|
|
||||||
Confirm: APIConfirm{Title: "Sell drill", Text: d.SellConfirm(), OK: "Sell for " + formatCredits(d.Resale), Danger: true}}
|
Confirm: APIConfirm{Title: "Sell drill", Text: d.SellConfirm(), OK: "Sell for " + formatCredits(d.Resale), Danger: true}}
|
||||||
for _, slot := range d.Slots {
|
|
||||||
a := APISlot{N: slot.N}
|
|
||||||
if slot.Gear != nil {
|
|
||||||
a.Attachment = slot.Gear.Name
|
|
||||||
}
|
|
||||||
dp.Slots = append(dp.Slots, a)
|
|
||||||
}
|
|
||||||
for _, sg := range d.Spares {
|
|
||||||
sp := APISpare{Name: sg.Name, Count: sg.Count, CycleSecs: sg.Secs, Yield: sg.Yield, CanEquip: sg.CanEquip,
|
|
||||||
WhyText: sg.Why, Price: sg.Price, Resale: sg.Resale(),
|
|
||||||
Confirm: APIConfirm{Title: "Sell " + sg.Name, Text: sg.Confirm(), OK: "Sell for " + formatCredits(sg.Resale()), Danger: true}}
|
|
||||||
switch {
|
|
||||||
case sg.CanEquip:
|
|
||||||
case strings.HasPrefix(sg.Why, "Already"):
|
|
||||||
sp.Why = "fitted"
|
|
||||||
default:
|
|
||||||
sp.Why = "full"
|
|
||||||
}
|
|
||||||
dp.Spares = append(dp.Spares, sp)
|
|
||||||
}
|
|
||||||
if d.IdleBest != nil {
|
if d.IdleBest != nil {
|
||||||
dp.IdleBest = d.IdleBest.ID
|
dp.IdleBest = d.IdleBest.ID
|
||||||
}
|
}
|
||||||
@@ -535,7 +465,7 @@ func apiEquipment(p *Page) *APIEquipment {
|
|||||||
type APIBuyModal struct {
|
type APIBuyModal struct {
|
||||||
ToolID string `json:"toolId"`
|
ToolID string `json:"toolId"`
|
||||||
StoreItemID int `json:"storeItemId"`
|
StoreItemID int `json:"storeItemId"`
|
||||||
CycleSecs int `json:"cycleSecs"`
|
CycleSecs float64 `json:"cycleSecs"`
|
||||||
Yield float64 `json:"yield"`
|
Yield float64 `json:"yield"`
|
||||||
Reach int `json:"reach"`
|
Reach int `json:"reach"`
|
||||||
NoMoons bool `json:"noMoons"`
|
NoMoons bool `json:"noMoons"`
|
||||||
@@ -580,7 +510,6 @@ func apiBuyModal(m *BuyModal) *APIBuyModal {
|
|||||||
|
|
||||||
type APIStore struct {
|
type APIStore struct {
|
||||||
Tools []APIStoreTool `json:"tools"`
|
Tools []APIStoreTool `json:"tools"`
|
||||||
Attachments []APIStoreGear `json:"attachments"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIStoreTool struct {
|
type APIStoreTool struct {
|
||||||
@@ -593,27 +522,13 @@ type APIStoreTool struct {
|
|||||||
CanAfford bool `json:"canAfford"`
|
CanAfford bool `json:"canAfford"`
|
||||||
Owned int `json:"owned"`
|
Owned int `json:"owned"`
|
||||||
Yield float64 `json:"yield"` // base, as the card shows
|
Yield float64 `json:"yield"` // base, as the card shows
|
||||||
CycleSecs int `json:"cycleSecs"` // base
|
CycleSecs float64 `json:"cycleSecs"` // base
|
||||||
Reach int `json:"reach"`
|
Reach int `json:"reach"`
|
||||||
Rate float64 `json:"rate"` // credits per hour it would earn, with advisors
|
Rate float64 `json:"rate"` // credits per hour it would earn, with advisors
|
||||||
BestRate float64 `json:"bestRate"` // the same for the player's best tool
|
BestRate float64 `json:"bestRate"` // the same for the player's best tool
|
||||||
WhyText string `json:"whyText"` // why Buy is disabled ("Need ₥X more"), "" if it isn't
|
WhyText string `json:"whyText"` // why Buy is disabled ("Need ₥X more"), "" if it isn't
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIStoreGear struct {
|
|
||||||
Name string `json:"name"` // buy-attachment item=<name>
|
|
||||||
Image string `json:"image"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Effect APIEffect `json:"effect"`
|
|
||||||
EffectText string `json:"effectText"`
|
|
||||||
Price float64 `json:"price"`
|
|
||||||
CanAfford bool `json:"canAfford"`
|
|
||||||
Spare int `json:"spare"`
|
|
||||||
Equipped int `json:"equipped"`
|
|
||||||
Resale float64 `json:"resale"`
|
|
||||||
WhyText string `json:"whyText"` // why Buy is disabled, "" if it isn't
|
|
||||||
}
|
|
||||||
|
|
||||||
// whyShort is the web's tooltip on a Buy button the player can't afford, "" when they can.
|
// whyShort is the web's tooltip on a Buy button the player can't afford, "" when they can.
|
||||||
func whyShort(canAfford bool, balance, price float64) string {
|
func whyShort(canAfford bool, balance, price float64) string {
|
||||||
if canAfford {
|
if canAfford {
|
||||||
@@ -624,17 +539,12 @@ func whyShort(canAfford bool, balance, price float64) string {
|
|||||||
|
|
||||||
func apiStore(p *Page) *APIStore {
|
func apiStore(p *Page) *APIStore {
|
||||||
bal := p.S.Balance
|
bal := p.S.Balance
|
||||||
st := &APIStore{Tools: []APIStoreTool{}, Attachments: []APIStoreGear{}}
|
st := &APIStore{Tools: []APIStoreTool{}}
|
||||||
for _, r := range p.Store {
|
for _, r := range p.Store {
|
||||||
st.Tools = append(st.Tools, APIStoreTool{StoreItemID: r.Item.ID, Name: r.Item.Name, Code: toolCode(r.Item.Name),
|
st.Tools = append(st.Tools, APIStoreTool{StoreItemID: r.Item.ID, Name: r.Item.Name, Code: toolCode(r.Item.Name),
|
||||||
Image: itemImages + r.Item.Image, Description: r.Item.Description, Price: r.Item.Price, CanAfford: r.CanAfford,
|
Image: itemImages + r.Item.Image, Description: r.Item.Description, Price: r.Item.Price, CanAfford: r.CanAfford,
|
||||||
Owned: r.Owned, Yield: r.Item.ClaimAmount, CycleSecs: r.Item.CompletionTimeInSecs,
|
Owned: r.Owned, Yield: r.Item.ClaimAmount, CycleSecs: float64(r.Item.CompletionTimeInSecs),
|
||||||
Reach: max(1, r.Item.ToolReach(0)), Rate: r.Rate, BestRate: r.BestRate, WhyText: whyShort(r.CanAfford, bal, r.Item.Price)})
|
Reach: r.Item.ToolReach(), Rate: r.Rate, BestRate: r.BestRate, WhyText: whyShort(r.CanAfford, bal, r.Item.Price)})
|
||||||
}
|
|
||||||
for _, g := range p.Gear {
|
|
||||||
st.Attachments = append(st.Attachments, APIStoreGear{Name: g.Name, Image: attachmentImages + g.Image,
|
|
||||||
Description: g.Description, Effect: effectOf(g.Speed, g.Yield, 0, 0), EffectText: g.Attachment.Effect(), Price: g.Price,
|
|
||||||
CanAfford: g.CanAfford, Spare: g.Spare, Equipped: g.Equipped, Resale: g.Resale(), WhyText: whyShort(g.CanAfford, bal, g.Price)})
|
|
||||||
}
|
}
|
||||||
return st
|
return st
|
||||||
}
|
}
|
||||||
@@ -911,7 +821,7 @@ func apiHelp(s *State) *APIHelp {
|
|||||||
"Pick a well on your moon and deploy a tool on it.",
|
"Pick a well on your moon and deploy a tool on it.",
|
||||||
`When the drill finishes, claim what it dug up. "Claim and redeploy" puts the tool straight back to work; after a claim, "Redrill" does the same.`,
|
`When the drill finishes, claim what it dug up. "Claim and redeploy" puts the tool straight back to work; after a claim, "Redrill" does the same.`,
|
||||||
"Sell resources in the Market, upgrade your equipment, claim more moons.",
|
"Sell resources in the Market, upgrade your equipment, claim more moons.",
|
||||||
"Buy attachments in the Store and equip them on a drill (click it in Equipment) to make it faster or bigger. Hire advisors to boost your whole operation.",
|
"Each drill has two upgrade tracks in Equipment: one raises what a cycle mines, the other makes cycles shorter. Hire advisors to boost your whole operation.",
|
||||||
}, Note: "Your progress is saved on this device.",
|
}, Note: "Your progress is saved on this device.",
|
||||||
NewGame: APIConfirm{Title: "Start over", Text: newGameConfirm, OK: "Start over", Danger: true}}
|
NewGame: APIConfirm{Title: "Start over", Text: newGameConfirm, OK: "Start over", Danger: true}}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-13
@@ -102,7 +102,7 @@ func TestAPI(t *testing.T) {
|
|||||||
t.Errorf("view %+v: %d stakes, %d tools", v, len(a.Stakes), len(a.Tools))
|
t.Errorf("view %+v: %d stakes, %d tools", v, len(a.Stakes), len(a.Tools))
|
||||||
}
|
}
|
||||||
for _, st := range a.Stakes {
|
for _, st := range a.Stakes {
|
||||||
if st.End-st.Start != int64(st.Secs)*1000 {
|
if st.End-st.Start != int64(math.Round(st.Secs*1000)) {
|
||||||
t.Errorf("stake %s: end - start is not its duration", st.ID)
|
t.Errorf("stake %s: end - start is not its duration", st.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,27 +191,28 @@ func checkAPITexts(t *testing.T, v View, a *APIState) {
|
|||||||
if len(c.Reach) != len(c.Resources) {
|
if len(c.Reach) != len(c.Resources) {
|
||||||
fail("reach texts", c.Reach)
|
fail("reach texts", c.Reach)
|
||||||
}
|
}
|
||||||
for _, at := range c.Attachments {
|
for _, tool := range c.Tools {
|
||||||
if at.EffectText == "" {
|
if len(tool.Tracks) != 2 || tool.Tracks[0].Kind != "yield" || tool.Tracks[1].Kind != "cycle" {
|
||||||
fail("attachment effect", at)
|
fail("tool tracks", tool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e := a.Equipment; e != nil {
|
if e := a.Equipment; e != nil {
|
||||||
for _, r := range e.Tools {
|
for _, r := range e.Tools {
|
||||||
if (r.NoSale != "") != (r.NoSaleText != "") || r.Next != nil && (r.Next.Blocked != "") != (r.Next.BlockedText != "") {
|
if (r.NoSale != "") != (r.NoSaleText != "") {
|
||||||
fail("tool row texts", r)
|
fail("tool row texts", r)
|
||||||
}
|
}
|
||||||
|
for _, tr := range r.Tracks {
|
||||||
|
if n := tr.Next; n != nil && ((n.Blocked != "") != (n.BlockedText != "") || n.Label == "") ||
|
||||||
|
len(tr.Ladder) != tr.Max+1 {
|
||||||
|
fail("track texts", tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if d := e.Drill; d != nil {
|
if d := e.Drill; d != nil {
|
||||||
if (d.NoSale != "") != (d.NoSaleText != "") || d.Confirm.Text == "" {
|
if (d.NoSale != "") != (d.NoSaleText != "") || d.Confirm.Text == "" {
|
||||||
fail("drill panel texts", d)
|
fail("drill panel texts", d)
|
||||||
}
|
}
|
||||||
for _, sp := range d.Spares {
|
|
||||||
if (sp.Why != "") != (sp.WhyText != "") || sp.Confirm.Text == "" {
|
|
||||||
fail("spare texts", sp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if st := a.Store; st != nil {
|
if st := a.Store; st != nil {
|
||||||
@@ -268,11 +269,9 @@ func checkAPISections(t *testing.T, s *State, v View, a *APIState) {
|
|||||||
t.Errorf("view %+v: equipment %+v", v, a.Equipment)
|
t.Errorf("view %+v: equipment %+v", v, a.Equipment)
|
||||||
} else if (a.Equipment.Drill != nil) != (v.Drill != "") {
|
} else if (a.Equipment.Drill != nil) != (v.Drill != "") {
|
||||||
t.Errorf("view %+v: drill panel %v", v, a.Equipment.Drill != nil)
|
t.Errorf("view %+v: drill panel %v", v, a.Equipment.Drill != nil)
|
||||||
} else if d := a.Equipment.Drill; d != nil && len(d.Slots) != cfg().Attachments.SlotsPerTool {
|
|
||||||
t.Errorf("view %+v: %d slots", v, len(d.Slots))
|
|
||||||
}
|
}
|
||||||
case "store":
|
case "store":
|
||||||
if a.Store == nil || len(a.Store.Tools) != len(s.Config.StoreItems) || len(a.Store.Attachments) != len(cfg().Attachments.Items) {
|
if a.Store == nil || len(a.Store.Tools) != len(s.Config.StoreItems) {
|
||||||
t.Errorf("view %+v: store %+v", v, a.Store)
|
t.Errorf("view %+v: store %+v", v, a.Store)
|
||||||
}
|
}
|
||||||
case "refinery":
|
case "refinery":
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# Drill attachment art
|
|
||||||
|
|
||||||
The attachment images (`web/static/assets/attachments/<image>`, `image` in
|
|
||||||
`config/game.json` `attachments.items`) are rendered in Blender (4.5 LTS, Cycles on
|
|
||||||
CPU) with the equipment pipeline: `art/equipment/lib.py` for materials, the regolith
|
|
||||||
ground, world, lights, camera and render settings.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
art/attachments/render_all.sh # all four
|
|
||||||
art/attachments/render_all.sh diamond-bit # just one
|
|
||||||
```
|
|
||||||
|
|
||||||
`render.py` builds and renders at 1024px, `finish.py` (system `python3` with Pillow)
|
|
||||||
writes the 512px PNGs and contact sheets in `build/`. `--res N` and `--samples N`
|
|
||||||
give quick previews.
|
|
||||||
|
|
||||||
`parts.py`: one build function per attachment and `PARTS` (key = image name, detail
|
|
||||||
scale `S`, ground seed).
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
"""Turn raw 1024px renders into the 512px game images, plus contact sheets.
|
|
||||||
|
|
||||||
python3 art/attachments/finish.py [RAW_DIR] [--sheet DIR]
|
|
||||||
|
|
||||||
Needs Pillow. Writes web/static/assets/attachments/<name>.png for every <name>.png in RAW_DIR
|
|
||||||
(the names are the `image` slugs in config/game.json).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
OUT = os.path.normpath(os.path.join(HERE, '..', '..', 'web', 'static', 'assets', 'attachments'))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
a = sys.argv[1:]
|
|
||||||
sheet_dir = None
|
|
||||||
if '--sheet' in a:
|
|
||||||
i = a.index('--sheet')
|
|
||||||
sheet_dir = a[i + 1]
|
|
||||||
del a[i:i + 2]
|
|
||||||
raw = a[0] if a else os.path.join(HERE, 'build')
|
|
||||||
os.makedirs(OUT, exist_ok=True)
|
|
||||||
imgs = []
|
|
||||||
for fname in sorted(f for f in os.listdir(raw) if f.endswith('.png') and not f.startswith('contact_sheet')):
|
|
||||||
im = Image.open(os.path.join(raw, fname)).convert('RGB').resize((512, 512), Image.LANCZOS)
|
|
||||||
out = os.path.join(OUT, fname)
|
|
||||||
im.save(out, 'PNG', optimize=True)
|
|
||||||
if os.path.getsize(out) > 200_000:
|
|
||||||
q = im.quantize(256, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.FLOYDSTEINBERG)
|
|
||||||
q.save(out, 'PNG', optimize=True)
|
|
||||||
print(f'{fname}: {os.path.getsize(out) // 1024} KB')
|
|
||||||
imgs.append(Image.open(out).convert('RGB'))
|
|
||||||
if sheet_dir and imgs:
|
|
||||||
os.makedirs(sheet_dir, exist_ok=True)
|
|
||||||
for size, fn in ((512, 'contact_sheet.png'), (200, 'contact_sheet_200px.png')):
|
|
||||||
pad = 8
|
|
||||||
sheet = Image.new('RGB', (len(imgs) * (size + pad) + pad, size + 2 * pad), (4, 10, 22))
|
|
||||||
for i, im in enumerate(imgs):
|
|
||||||
sheet.paste(im.resize((size, size), Image.LANCZOS), (pad + i * (size + pad), pad))
|
|
||||||
sheet.save(os.path.join(sheet_dir, fn))
|
|
||||||
print('contact sheets in', sheet_dir)
|
|
||||||
|
|
||||||
|
|
||||||
main()
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
"""The four drill attachments. Each build function draws into a lib.Builder (art/equipment).
|
|
||||||
|
|
||||||
Coordinates: metres, Z up, front faces -Y (the camera sits front-right, +X/-Y).
|
|
||||||
Keys are the image names (`image` in config/game.json `attachments.items`, minus .png).
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
|
|
||||||
from lib import CYAN, AMBER
|
|
||||||
|
|
||||||
PI = math.pi
|
|
||||||
|
|
||||||
|
|
||||||
# Turbo Core: an overdriven motor core. Finned housing, glowing copper windings, flange.
|
|
||||||
def turbo_core(b):
|
|
||||||
z0 = 0.05
|
|
||||||
# mounting flange and feet
|
|
||||||
b.cyl(0.17, 0.035, 'graphite', (0, 0, z0 + 0.0175))
|
|
||||||
for k in range(8):
|
|
||||||
a = k / 8 * 2 * PI + PI / 8
|
|
||||||
b.cyl(0.011, 0.012, 'chrome', (math.cos(a) * 0.145, math.sin(a) * 0.145, z0 + 0.04), seg=8)
|
|
||||||
for sx in (-1, 1):
|
|
||||||
b.box((0.06, 0.32, 0.05), 'dark', (sx * 0.14, 0, 0.025), bevel=0.008)
|
|
||||||
# housing: lower stator drum with radial fins
|
|
||||||
zc = z0 + 0.035
|
|
||||||
b.cyl(0.12, 0.22, 'steel', (0, 0, zc + 0.11))
|
|
||||||
for k in range(20):
|
|
||||||
a = k / 20 * 2 * PI
|
|
||||||
with b.at((0, 0, zc + 0.11), (0, 0, a)):
|
|
||||||
b.box((0.05, 0.008, 0.2), 'graphite', (0.135, 0, 0), bevel=0.002)
|
|
||||||
for z in (zc + 0.015, zc + 0.205):
|
|
||||||
b.torus(0.155, 0.012, 'graphite', (0, 0, z))
|
|
||||||
# exposed windings: glowing core behind a cage
|
|
||||||
zw = zc + 0.22
|
|
||||||
b.cyl(0.085, 0.13, 'core_hot', (0, 0, zw + 0.065))
|
|
||||||
b.helix((0, 0), 0.092, zw + 0.008, zw + 0.122, 7, 0.008, 'brass')
|
|
||||||
for k in range(8):
|
|
||||||
a = k / 8 * 2 * PI
|
|
||||||
b.strut((math.cos(a) * 0.105, math.sin(a) * 0.105, zw), (math.cos(a) * 0.105, math.sin(a) * 0.105, zw + 0.13),
|
|
||||||
0.006, 'chrome', seg=8)
|
|
||||||
b.light((0, -0.16, zw + 0.07), AMBER, 6, 0.03)
|
|
||||||
b.light((0.16, 0.0, zw + 0.07), AMBER, 3, 0.03)
|
|
||||||
# cap with turbine fan and shaft coupling
|
|
||||||
zt = zw + 0.13
|
|
||||||
b.cyl(0.115, 0.03, 'white', (0, 0, zt + 0.015))
|
|
||||||
b.cyl(0.08, 0.02, 'dark', (0, 0, zt + 0.04))
|
|
||||||
for k in range(12):
|
|
||||||
a = k / 12 * 2 * PI
|
|
||||||
with b.at((0, 0, zt + 0.045), (0, 0, a)):
|
|
||||||
b.box((0.065, 0.004, 0.012), 'metal', (0.04, 0, 0), (0.6, 0, 0), bevel=0.001)
|
|
||||||
b.cyl(0.03, 0.08, 'chrome', (0, 0, zt + 0.08))
|
|
||||||
b.cyl(0.04, 0.02, 'graphite', (0, 0, zt + 0.125))
|
|
||||||
for k in range(4):
|
|
||||||
a = k / 4 * 2 * PI
|
|
||||||
b.box((0.012, 0.012, 0.02), 'chrome', (math.cos(a) * 0.04, math.sin(a) * 0.04, zt + 0.14), (0, 0, a), bevel=0.002)
|
|
||||||
# status lamp, label plate and cables
|
|
||||||
b.box((0.08, 0.006, 0.04), 'white', (0.0, -0.157, zc + 0.12), bevel=0.002)
|
|
||||||
b.box((0.05, 0.004, 0.006), 'amber', (0.0, -0.161, zc + 0.12), bevel=0)
|
|
||||||
b.lamp((0.1, -0.1, zt + 0.035), 0.012, 'red', housing=False)
|
|
||||||
b.tube([(-0.12, 0.08, zw + 0.03), (-0.2, 0.12, zw - 0.05), (-0.24, 0.05, 0.02)], 0.012, 'rubber')
|
|
||||||
b.tube([(-0.12, 0.03, zw + 0.06), (-0.22, 0.02, zw - 0.02), (-0.26, -0.06, 0.02)], 0.009, 'orange')
|
|
||||||
|
|
||||||
|
|
||||||
# Diamond Bit: a fluted drill bit with a diamond-studded cutting crown, upright on its shank.
|
|
||||||
def diamond_bit(b):
|
|
||||||
# shank in a coupling block on the ground
|
|
||||||
b.box((0.2, 0.2, 0.05), 'graphite', (0, 0, 0.025), bevel=0.01)
|
|
||||||
b.hazard_band(0.21, 0.21, 0.015, (0, 0, 0.055))
|
|
||||||
b.cyl(0.065, 0.08, 'steel', (0, 0, 0.1))
|
|
||||||
for k in range(6):
|
|
||||||
a = k / 6 * 2 * PI
|
|
||||||
b.box((0.018, 0.035, 0.07), 'metal', (math.cos(a) * 0.066, math.sin(a) * 0.066, 0.1), (0, 0, a), bevel=0.004)
|
|
||||||
# fluted body
|
|
||||||
z0, z1 = 0.14, 0.44
|
|
||||||
b.cyl(0.045, z1 - z0, 'graphite', (0, 0, (z0 + z1) / 2))
|
|
||||||
for k in range(2):
|
|
||||||
pts = []
|
|
||||||
n = 48
|
|
||||||
for i in range(n + 1):
|
|
||||||
t = i / n
|
|
||||||
a = k * PI + t * 2.6 * PI
|
|
||||||
pts.append((math.cos(a) * 0.055, math.sin(a) * 0.055, z0 + (z1 - z0) * t))
|
|
||||||
b.tube(pts, 0.024, 'metal', seg=12, steps=2)
|
|
||||||
# crown: carbide ring with diamond studs, cone tip
|
|
||||||
zc = z1 + 0.01
|
|
||||||
b.lathe([(0.07, zc - 0.03), (0.09, zc), (0.09, zc + 0.035), (0.08, zc + 0.045), (0.0, zc + 0.045)], 'graphite')
|
|
||||||
b.lathe([(0.078, zc + 0.045), (0.05, zc + 0.09), (0.015, zc + 0.13), (0.0, zc + 0.135)], 'steel')
|
|
||||||
for k in range(12):
|
|
||||||
a = k / 12 * 2 * PI
|
|
||||||
b.sphere(0.014, 'diamond', (math.cos(a) * 0.089, math.sin(a) * 0.089, zc + 0.02), seg=6)
|
|
||||||
for k in range(7):
|
|
||||||
a = k / 7 * 2 * PI + 0.3
|
|
||||||
b.sphere(0.011, 'diamond', (math.cos(a) * 0.05, math.sin(a) * 0.05, zc + 0.088), seg=6)
|
|
||||||
b.sphere(0.01, 'diamond', (0, 0, zc + 0.136), seg=6)
|
|
||||||
b.light((0.0, -0.15, zc + 0.06), CYAN, 3, 0.02)
|
|
||||||
# a couple of loose diamonds on the ground
|
|
||||||
for x, y in ((0.16, -0.1), (-0.13, -0.15), (0.07, -0.17)):
|
|
||||||
b.sphere(0.013, 'diamond', (x, y, 0.008), seg=6)
|
|
||||||
|
|
||||||
|
|
||||||
# Cryo Cooler: a coolant jacket that wraps the spindle. Frosted canister, glowing coolant
|
|
||||||
# windows, coiled lines and a gauge.
|
|
||||||
def cryo_cooler(b):
|
|
||||||
b.cyl(0.16, 0.03, 'graphite', (0, 0, 0.015))
|
|
||||||
b.cyl(0.15, 0.015, 'cyan_soft', (0, 0, 0.035))
|
|
||||||
# jacket
|
|
||||||
z0, z1 = 0.045, 0.34
|
|
||||||
b.cyl(0.11, z1 - z0, 'frost', (0, 0, (z0 + z1) / 2))
|
|
||||||
for z in (z0 + 0.02, (z0 + z1) / 2, z1 - 0.02):
|
|
||||||
b.torus(0.112, 0.012, 'steel', (0, 0, z))
|
|
||||||
for k in range(4):
|
|
||||||
a = k / 4 * 2 * PI + PI / 4
|
|
||||||
with b.at((0, 0, 0), (0, 0, a)):
|
|
||||||
b.box((0.012, 0.04, 0.1), 'coolant', (0.0, -0.104, z0 + 0.075), bevel=0.003)
|
|
||||||
b.box((0.012, 0.04, 0.1), 'coolant', (0.0, -0.104, z1 - 0.075), bevel=0.003)
|
|
||||||
b.light((0, -0.25, 0.19), CYAN, 8, 0.05)
|
|
||||||
# spindle through the middle
|
|
||||||
b.cyl(0.035, 0.12, 'chrome', (0, 0, z1 + 0.06))
|
|
||||||
b.cyl(0.075, 0.03, 'white', (0, 0, z1 + 0.015))
|
|
||||||
b.cyl(0.045, 0.02, 'graphite', (0, 0, z1 + 0.12))
|
|
||||||
# coolant coil around the jacket and feed lines
|
|
||||||
b.helix((0, 0), 0.128, z0 + 0.04, z1 - 0.04, 4, 0.007, 'chrome')
|
|
||||||
b.tube([(0.12, 0.05, z1 - 0.04), (0.2, 0.08, z1 + 0.02), (0.24, 0.12, 0.1), (0.26, 0.2, 0.02)], 0.014, 'white')
|
|
||||||
b.tube([(-0.12, 0.05, z0 + 0.05), (-0.2, 0.1, 0.05), (-0.22, 0.2, 0.02)], 0.012, 'rubber')
|
|
||||||
# pressure gauge
|
|
||||||
with b.at((-0.08, -0.1, z1 - 0.05), (0, 0, -0.6)):
|
|
||||||
b.cyl(0.03, 0.02, 'steel', (0, -0.02, 0), axis='y')
|
|
||||||
b.cyl(0.024, 0.005, 'white', (0, -0.031, 0), axis='y')
|
|
||||||
b.box((0.003, 0.002, 0.02), 'red', (0.005, -0.034, 0.004), (0, 0.6, 0), bevel=0)
|
|
||||||
# frost chips on the ground
|
|
||||||
for x, y, r in ((0.18, -0.06, 0.016), (-0.17, -0.1, 0.012), (0.08, -0.18, 0.01)):
|
|
||||||
b.sphere(r, 'ice', (x, y, r * 0.4), seg=10, scale=(1.2, 1.0, 0.6))
|
|
||||||
|
|
||||||
|
|
||||||
# Seam Scanner: ground-penetrating radar on a sled. Antenna panel facing the ground,
|
|
||||||
# rotating dish, a scan display.
|
|
||||||
def seam_scanner(b):
|
|
||||||
# sled runners
|
|
||||||
for sx in (-1, 1):
|
|
||||||
b.box((0.03, 0.42, 0.025), 'dark', (sx * 0.15, 0, 0.0125), bevel=0.008)
|
|
||||||
b.strut((sx * 0.15, -0.21, 0.012), (sx * 0.15, -0.25, 0.05), 0.013, 'dark')
|
|
||||||
# radar body with glowing underside emitter
|
|
||||||
b.box((0.36, 0.34, 0.08), 'white', (0, 0, 0.075), bevel=0.02)
|
|
||||||
b.box((0.3, 0.28, 0.02), 'cyan', (0, 0, 0.032), bevel=0.004)
|
|
||||||
b.light((0, 0, 0.01), CYAN, 5, 0.1)
|
|
||||||
b.hazard_band(0.37, 0.02, 0.02, (0, -0.17, 0.085))
|
|
||||||
# electronics box
|
|
||||||
b.box((0.22, 0.2, 0.1), 'graphite', (0.02, 0.04, 0.165), bevel=0.012)
|
|
||||||
b.vent(0.12, 0.05, (0.131, 0.06, 0.165), (0, 0, PI / 2))
|
|
||||||
# display with a scan grid
|
|
||||||
with b.at((-0.02, -0.08, 0.23), (-0.5, 0, 0)):
|
|
||||||
b.box((0.16, 0.012, 0.11), 'dark', (0, 0, 0), bevel=0.006)
|
|
||||||
b.box((0.14, 0.004, 0.09), 'glass', (0, -0.007, 0), bevel=0)
|
|
||||||
# mast with a dish
|
|
||||||
b.cyl(0.018, 0.16, 'steel', (0.08, 0.1, 0.29))
|
|
||||||
b.cyl(0.03, 0.02, 'graphite', (0.08, 0.1, 0.37))
|
|
||||||
b.lathe([(0.0, 0.0), (0.05, 0.012), (0.09, 0.04)], 'white', (0.08, 0.1, 0.39), (0.5, 0.0, -0.4))
|
|
||||||
b.strut((0.08, 0.1, 0.39), (0.08 - 0.02, 0.1 - 0.035, 0.45), 0.004, 'graphite')
|
|
||||||
b.lamp((0.06, 0.065, 0.455), 0.01, 'cyan', housing=False)
|
|
||||||
# antenna whips and lamp
|
|
||||||
b.strut((-0.13, 0.12, 0.215), (-0.15, 0.14, 0.42), 0.004, 'graphite')
|
|
||||||
b.lamp((-0.15, 0.14, 0.425), 0.009, 'amber', housing=False)
|
|
||||||
b.lamp((0.14, -0.1, 0.13), 0.01, 'amber')
|
|
||||||
# cable reel at the back
|
|
||||||
b.cyl(0.05, 0.05, 'orange', (-0.1, 0.13, 0.17), axis='x')
|
|
||||||
b.cyl(0.035, 0.055, 'rubber', (-0.1, 0.13, 0.17), axis='x')
|
|
||||||
|
|
||||||
|
|
||||||
PARTS = {
|
|
||||||
'turbo-core': {'build': turbo_core, 'S': 0.3, 'seed': 11},
|
|
||||||
'diamond-bit': {'build': diamond_bit, 'S': 0.25, 'seed': 12, 'fill': 0.8},
|
|
||||||
'cryo-cooler': {'build': cryo_cooler, 'S': 0.3, 'seed': 13},
|
|
||||||
'seam-scanner': {'build': seam_scanner, 'S': 0.3, 'seed': 14},
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
"""Build and render the drill attachment icons.
|
|
||||||
|
|
||||||
blender -b --factory-startup --python art/attachments/render.py -- [names...|all] [--out DIR]
|
|
||||||
[--res N] [--samples N]
|
|
||||||
|
|
||||||
Raw renders go to --out (default art/attachments/build); finish.py turns them into the
|
|
||||||
game images. Uses art/equipment/lib.py, so the parts match the store equipment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
sys.path.insert(0, HERE)
|
|
||||||
sys.path.insert(0, os.path.join(HERE, '..', 'equipment'))
|
|
||||||
|
|
||||||
import lib # noqa: E402
|
|
||||||
import parts # noqa: E402
|
|
||||||
from lib import srgb # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
def args():
|
|
||||||
a = sys.argv[sys.argv.index('--') + 1:] if '--' in sys.argv else []
|
|
||||||
opts = {'out': os.path.join(HERE, 'build'), 'res': 1024, 'samples': 96}
|
|
||||||
names = []
|
|
||||||
i = 0
|
|
||||||
while i < len(a):
|
|
||||||
k = a[i]
|
|
||||||
if k == '--out':
|
|
||||||
opts['out'] = a[i + 1]; i += 2
|
|
||||||
elif k in ('--res', '--samples'):
|
|
||||||
opts[k[2:]] = int(a[i + 1]); i += 2
|
|
||||||
else:
|
|
||||||
names.append(k); i += 1
|
|
||||||
if not names or names == ['all']:
|
|
||||||
names = list(parts.PARTS)
|
|
||||||
return names, opts
|
|
||||||
|
|
||||||
|
|
||||||
def materials(S):
|
|
||||||
M = lib.make_materials(S)
|
|
||||||
M['core_hot'] = lib.emissive('core_hot', srgb('#ff8a2b'), 9.0, base=srgb('#5a2a0a'))
|
|
||||||
M['diamond'] = lib.simple('diamond', srgb('#e8f8ff'), metal=0.3, rough=0.03, coat=1.0,
|
|
||||||
emission=srgb('#9fe8ff'), estrength=0.35)
|
|
||||||
M['frost'] = lib.paint('frost', srgb('#e4f1f8'), S, rough=0.55, panels=False, wear=0.2, dirt=0.25)
|
|
||||||
M['coolant'] = lib.emissive('coolant', srgb('#5fd4ff'), 5.0, base=srgb('#10303c'))
|
|
||||||
M['ice'] = lib.simple('ice', srgb('#cfeefc'), rough=0.15, coat=1.0, emission=srgb('#5fd4ff'), estrength=0.3)
|
|
||||||
return M
|
|
||||||
|
|
||||||
|
|
||||||
def build_scene(name):
|
|
||||||
spec = parts.PARTS[name]
|
|
||||||
lib.reset()
|
|
||||||
coll = bpy.context.scene.collection
|
|
||||||
S = spec['S']
|
|
||||||
b = lib.Builder(name, materials(S), S)
|
|
||||||
spec['build'](b)
|
|
||||||
objs = b.finish(coll)
|
|
||||||
return spec, objs, coll
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
names, o = args()
|
|
||||||
for name in names:
|
|
||||||
t0 = time.time()
|
|
||||||
spec, objs, coll = build_scene(name)
|
|
||||||
pts = lib.machine_points(objs)
|
|
||||||
lo = [min(p[i] for p in pts) for i in range(3)]
|
|
||||||
hi = [max(p[i] for p in pts) for i in range(3)]
|
|
||||||
span = max(hi[0] - lo[0], hi[1] - lo[1])
|
|
||||||
lib.build_ground(coll, spec['S'], span * spec.get('ground', 0.9), ((lo[0], lo[1]), (hi[0], hi[1])),
|
|
||||||
seed=spec.get('seed', 3))
|
|
||||||
lib.build_world()
|
|
||||||
lib.add_lights(coll)
|
|
||||||
lib.setup_camera(coll, pts, fill=spec.get('fill', 0.78), aim=spec.get('aim', (0.5, 0.5)))
|
|
||||||
lib.setup_render(o['res'], o['samples'])
|
|
||||||
os.makedirs(o['out'], exist_ok=True)
|
|
||||||
sc = bpy.context.scene
|
|
||||||
sc.render.filepath = os.path.join(o['out'], name + '.png')
|
|
||||||
bpy.ops.render.render(write_still=True)
|
|
||||||
print(f'[attachments] {name}: {time.time() - t0:.1f}s', flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
main()
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Render every attachments image (or the named ones) and write the game images.
|
|
||||||
# art/attachments/render_all.sh [name ...]
|
|
||||||
set -e
|
|
||||||
here=$(cd "$(dirname "$0")" && pwd)
|
|
||||||
blender=${BLENDER:-blender}
|
|
||||||
[ $# -eq 0 ] && set -- all
|
|
||||||
"$blender" -b --factory-startup --python "$here/render.py" -- "$@" --out "$here/build"
|
|
||||||
python3 "$here/finish.py" "$here/build" --sheet "$here/build"
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// Drill attachments: equipment bought in the Store that goes into the player's inventory,
|
|
||||||
// then is equipped on one drill in the Equipment section (the drill's side panel). An
|
|
||||||
// attachment's modifier makes the drill's stakes faster (speed) or bigger (yield). A drill
|
|
||||||
// has attachments.slotsPerTool slots and takes at most one of each kind. Equipping and
|
|
||||||
// unequipping work any time, also while the drill mines: like every bonus (bonus.go), it
|
|
||||||
// counts from the drill's next stake. Table attachment (inventory_item_id null: unequipped).
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AttachmentConfig struct {
|
|
||||||
SlotsPerTool int `json:"slotsPerTool"`
|
|
||||||
Items []Attachment `json:"items"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Attachment struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Image string `json:"image"` // in web/static/assets/attachments
|
|
||||||
Price float64 `json:"price"`
|
|
||||||
Speed float64 `json:"speed,omitempty"` // modifiers (multipliers); unset is 1
|
|
||||||
Yield float64 `json:"yield,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GameConfig) attachment(name string) *Attachment {
|
|
||||||
for i := range c.Attachments.Items {
|
|
||||||
if c.Attachments.Items[i].Name == name {
|
|
||||||
return &c.Attachments.Items[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Effect describes the modifier, e.g. "+20% speed".
|
|
||||||
func (a *Attachment) Effect() string {
|
|
||||||
var parts []string
|
|
||||||
if a.Speed > 1 {
|
|
||||||
parts = append(parts, pctText(a.Speed)+" speed")
|
|
||||||
}
|
|
||||||
if a.Yield > 1 {
|
|
||||||
parts = append(parts, pctText(a.Yield)+" yield")
|
|
||||||
}
|
|
||||||
return strings.Join(parts, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// buyAttachment buys an attachment into the player's inventory (unequipped).
|
|
||||||
func buyAttachment(ctx context.Context, db *pgxpool.Pool, userID, name string) error {
|
|
||||||
a := cfg().attachment(name)
|
|
||||||
if a == nil {
|
|
||||||
return gameErr("That attachment isn't for sale")
|
|
||||||
}
|
|
||||||
return inTx(ctx, db, func(tx pgx.Tx) error {
|
|
||||||
if err := debit(ctx, tx, userID, a.Price); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err := tx.Exec(ctx, "insert into attachment(user_id, item) values ($1, $2)", userID, a.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := unlock(ctx, tx, userID, "advisors"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return logEvent(ctx, tx, userID, "buy_attachment", map[string]any{"item": a.Name, "price": a.Price})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// lockTool locks one of the player's tools.
|
|
||||||
func lockTool(ctx context.Context, tx pgx.Tx, userID, toolID string) error {
|
|
||||||
if !uuidPattern.MatchString(toolID) {
|
|
||||||
return gameErr("Tool not found")
|
|
||||||
}
|
|
||||||
var id string
|
|
||||||
err := tx.QueryRow(ctx, `
|
|
||||||
select id from inventory_item where id = $1 and user_id = $2 for update`, toolID, userID).Scan(&id)
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return gameErr("Tool not found")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// equipAttachment puts one of the player's spare attachments of a kind on a tool (an idle
|
|
||||||
// copy of it, if there is one: see pickTool) and returns the tool it went on.
|
|
||||||
func equipAttachment(ctx context.Context, db *pgxpool.Pool, userID, name, toolID string) (string, error) {
|
|
||||||
a := cfg().attachment(name)
|
|
||||||
if a == nil {
|
|
||||||
return "", gameErr("Unknown attachment")
|
|
||||||
}
|
|
||||||
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
|
||||||
// The tool row first, then its attachments.
|
|
||||||
var err error
|
|
||||||
if toolID, err = pickTool(ctx, tx, userID, toolID, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rows, err := tx.Query(ctx, `
|
|
||||||
select item from attachment where inventory_item_id = $1 for update`, toolID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fitted, err := pgx.CollectRows(rows, pgx.RowTo[string])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, f := range fitted {
|
|
||||||
if f == a.Name {
|
|
||||||
return gameErr("This drill already has a %s", a.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(fitted) >= cfg().Attachments.SlotsPerTool {
|
|
||||||
return gameErr("This drill's slots are full. Unequip one first")
|
|
||||||
}
|
|
||||||
var id string
|
|
||||||
err = tx.QueryRow(ctx, `
|
|
||||||
select id from attachment
|
|
||||||
where user_id = $1 and item = $2 and inventory_item_id is null
|
|
||||||
limit 1 for update skip locked`, userID, a.Name).Scan(&id)
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return gameErr("You have no spare %s. Buy one in the store", a.Name)
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(ctx, "update attachment set inventory_item_id = $2 where id = $1", id, toolID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return logEvent(ctx, tx, userID, "equip", map[string]any{"item": a.Name, "tool": toolID})
|
|
||||||
})
|
|
||||||
return toolID, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// unequipAttachment takes an attachment of a kind off a tool (an idle copy of it, if there
|
|
||||||
// is one), back into the inventory, and returns the tool it came off.
|
|
||||||
func unequipAttachment(ctx context.Context, db *pgxpool.Pool, userID, name, toolID string) (string, error) {
|
|
||||||
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
|
||||||
var err error
|
|
||||||
if toolID, err = pickTool(ctx, tx, userID, toolID, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tag, err := tx.Exec(ctx, `
|
|
||||||
update attachment set inventory_item_id = null
|
|
||||||
where inventory_item_id = $1 and item = $2`, toolID, name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if tag.RowsAffected() == 0 {
|
|
||||||
return gameErr("That attachment isn't on this drill")
|
|
||||||
}
|
|
||||||
return logEvent(ctx, tx, userID, "unequip", map[string]any{"item": name, "tool": toolID})
|
|
||||||
})
|
|
||||||
return toolID, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------ view
|
|
||||||
|
|
||||||
// gear is the attachments equipped on a tool, in config order.
|
|
||||||
func (s *State) gear(toolID string) []*Attachment {
|
|
||||||
var out []*Attachment
|
|
||||||
for i := range cfg().Attachments.Items {
|
|
||||||
a := &cfg().Attachments.Items[i]
|
|
||||||
for _, name := range s.Fitted[toolID] {
|
|
||||||
if name == a.Name {
|
|
||||||
out = append(out, a)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// AttachmentCard is an attachment in the store.
|
|
||||||
type AttachmentCard struct {
|
|
||||||
*Attachment
|
|
||||||
Spare, Equipped int
|
|
||||||
CanAfford bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) attachmentCards() []AttachmentCard {
|
|
||||||
var cards []AttachmentCard
|
|
||||||
for i := range cfg().Attachments.Items {
|
|
||||||
a := &cfg().Attachments.Items[i]
|
|
||||||
card := AttachmentCard{Attachment: a, Spare: s.Spare[a.Name], CanAfford: s.Balance >= a.Price}
|
|
||||||
for _, names := range s.Fitted {
|
|
||||||
for _, n := range names {
|
|
||||||
if n == a.Name {
|
|
||||||
card.Equipped++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cards = append(cards, card)
|
|
||||||
}
|
|
||||||
return cards
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrillPanel is the side panel for a drill selected in the Equipment section: its slots
|
|
||||||
// and the spare attachments that fit, with the stats they'd give.
|
|
||||||
type DrillPanel struct {
|
|
||||||
ToolRow
|
|
||||||
Slots []SlotGear
|
|
||||||
Spares []SpareGear
|
|
||||||
Mining bool
|
|
||||||
Resale float64 // what it sells for (resale.go)
|
|
||||||
NoSale string // why it can't be sold, if it can't
|
|
||||||
|
|
||||||
// While it mines: the idle tools of its type, the best one and its best well.
|
|
||||||
IdleSame int
|
|
||||||
IdleBest *Tool
|
|
||||||
DeployTo *Suggestion
|
|
||||||
IdleURL string // opens IdleBest's panel
|
|
||||||
|
|
||||||
// Store: the Store is unlocked, so the panel offers upgrades, attachments and selling
|
|
||||||
// (actions gated on "store", onboarding.go). Before that it shows stats and tiers only.
|
|
||||||
Store bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// SlotGear is one attachment slot; Gear is nil when it's empty.
|
|
||||||
type SlotGear struct {
|
|
||||||
N int
|
|
||||||
Gear *Attachment
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpareGear is a kind of spare attachment and the drill's stats with it equipped.
|
|
||||||
type SpareGear struct {
|
|
||||||
*Attachment
|
|
||||||
Count int
|
|
||||||
Secs int
|
|
||||||
Yield float64
|
|
||||||
CanEquip bool
|
|
||||||
Why string // why it can't be equipped
|
|
||||||
}
|
|
||||||
|
|
||||||
// noSale is why a tool can't be sold (only-tool, claim-first, mining), "" if it can.
|
|
||||||
func (s *State) noSale(row ToolRow) string {
|
|
||||||
switch {
|
|
||||||
case len(s.Tools) <= 1:
|
|
||||||
return "only-tool"
|
|
||||||
case row.Stake != nil && row.Stake.Ready:
|
|
||||||
return "claim-first"
|
|
||||||
case row.Stake != nil:
|
|
||||||
return "mining"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm is the question before selling one (the Sell button's confirm dialog).
|
|
||||||
func (sg SpareGear) Confirm() string {
|
|
||||||
return fmt.Sprintf("Sell a spare %s for %s? You paid %s.", sg.Name, formatCredits(sg.Resale()), formatCredits(sg.Price))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SellConfirm is the question before selling the drill (its Sell button's confirm dialog).
|
|
||||||
func (d *DrillPanel) SellConfirm() string {
|
|
||||||
q := fmt.Sprintf("Sell this %s (T%d) for %s?", d.Item.Name, d.Tier, formatCredits(d.Resale))
|
|
||||||
if len(d.Gear) > 0 {
|
|
||||||
q += " Its attachments go back to your inventory."
|
|
||||||
}
|
|
||||||
return q
|
|
||||||
}
|
|
||||||
|
|
||||||
var noSaleText = map[string]string{
|
|
||||||
"only-tool": "You can't sell your only tool",
|
|
||||||
"claim-first": "Claim its haul first",
|
|
||||||
"mining": "Still mining. Sell it once it's idle",
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) drillPanel(row ToolRow) *DrillPanel {
|
|
||||||
d := &DrillPanel{ToolRow: row, Mining: row.Stake != nil, Resale: row.Item.ToolResale(row.Tier),
|
|
||||||
Store: s.Unlocked("store")}
|
|
||||||
d.NoSale = noSaleText[s.noSale(row)]
|
|
||||||
for i := range cfg().Attachments.SlotsPerTool {
|
|
||||||
slot := SlotGear{N: i + 1}
|
|
||||||
if i < len(row.Gear) {
|
|
||||||
slot.Gear = row.Gear[i]
|
|
||||||
}
|
|
||||||
d.Slots = append(d.Slots, slot)
|
|
||||||
}
|
|
||||||
for i := range cfg().Attachments.Items {
|
|
||||||
a := &cfg().Attachments.Items[i]
|
|
||||||
n := s.Spare[a.Name]
|
|
||||||
if n == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sg := SpareGear{Attachment: a, Count: n, CanEquip: true}
|
|
||||||
names := append([]string{a.Name}, s.Fitted[row.ID]...)
|
|
||||||
sg.Secs, sg.Yield = drillStats(row.Item.ToolSecs(row.Tier), row.Item.ToolYield(row.Tier),
|
|
||||||
cfg().bonusesFor(s.Advisors, names), s.CycleBoosts.For("all"), s.YieldBoosts.For(""))
|
|
||||||
for _, g := range row.Gear {
|
|
||||||
if g.Name == a.Name {
|
|
||||||
sg.CanEquip, sg.Why = false, "Already on this drill"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sg.CanEquip && row.FreeSlots == 0 {
|
|
||||||
sg.CanEquip, sg.Why = false, "Slots full. Unequip one first"
|
|
||||||
}
|
|
||||||
d.Spares = append(d.Spares, sg)
|
|
||||||
}
|
|
||||||
if d.Mining {
|
|
||||||
// Highest tier, then most attachments: the one pickTool would deploy.
|
|
||||||
for i := range s.Tools {
|
|
||||||
t := &s.Tools[i]
|
|
||||||
if t.StoreItemID != row.StoreItemID || s.StakeForTool(t.ID) != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
d.IdleSame++
|
|
||||||
if b := d.IdleBest; b == nil || t.Tier > b.Tier ||
|
|
||||||
(t.Tier == b.Tier && len(s.Fitted[t.ID]) > len(s.Fitted[b.ID])) {
|
|
||||||
d.IdleBest = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if d.IdleBest != nil {
|
|
||||||
d.IdleURL = View{Section: "equipment", Drill: d.IdleBest.ID}.URL()
|
|
||||||
if m := s.buySuggestions(d.IdleBest.ID); m != nil && len(m.Suggestions) > 0 {
|
|
||||||
d.DeployTo = &m.Suggestions[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
+3
-3
@@ -27,9 +27,9 @@ func runSim(args []string) error {
|
|||||||
for _, t := range a.Tools {
|
for _, t := range a.Tools {
|
||||||
fmt.Printf("%-38s %8.0f %5d %6.0f %5d %7.0f %8.0f %8s %7s\n", t.Name, t.Price, t.Secs, t.Claim,
|
fmt.Printf("%-38s %8.0f %5d %6.0f %5d %7.0f %8.0f %8s %7s\n", t.Name, t.Price, t.Secs, t.Claim,
|
||||||
t.Reach, t.Eff, t.CreditsPerHour, t.Payback, t.Ratio)
|
t.Reach, t.Eff, t.CreditsPerHour, t.Payback, t.Ratio)
|
||||||
for _, u := range t.Tiers {
|
for _, u := range t.Levels {
|
||||||
fmt.Printf(" tier %d %6.0f + parts %5.0f yield %5.0f wells %d on moon %5.0f %8.0f ₥/h payback %s\n",
|
fmt.Printf(" %-5s %d %8.0f + parts %5.0f yield %5.0f cycle %4.1fs on moon %5.0f %9.0f ₥/h payback %s\n",
|
||||||
u.Tier, u.Price, u.PartsValue, u.Yield, u.Reach, u.Eff, u.CreditsPerHour, u.Payback)
|
u.Track, u.Level, u.Price, u.PartsValue, u.Yield, u.Secs, u.Eff, u.CreditsPerHour, u.Payback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Println("\nrefinery")
|
fmt.Println("\nrefinery")
|
||||||
|
|||||||
+23
-71
@@ -1,8 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
// Player bonuses: drill attachments (attachments.go, equipped on one tool; their bonus is
|
// Player bonuses: advisors (advisors.go, for the whole operation). Each bonus is a
|
||||||
// the modifier) and advisors (advisors.go, for the whole operation). Each bonus is a
|
// multiplier (1.1 = +10%). A player's advisors multiply, then each kind is capped by bonusCaps
|
||||||
// multiplier (1.1 = +10%). Within a player's attachments and advisors they multiply, then each kind is capped by bonusCaps
|
|
||||||
// in the config; live events (events.go) multiply on top, uncapped. Bonuses are applied
|
// in the config; live events (events.go) multiply on top, uncapped. Bonuses are applied
|
||||||
// when an action runs: a stake fixes its duration and amount when it starts.
|
// when an action runs: a stake fixes its duration and amount when it starts.
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ import (
|
|||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BonusCaps are the highest combined multipliers a player's attachments and advisors give.
|
// BonusCaps are the highest combined multipliers a player's advisors give.
|
||||||
type BonusCaps struct {
|
type BonusCaps struct {
|
||||||
Speed float64 `json:"speed"` // drill speed: cycle duration is divided by it
|
Speed float64 `json:"speed"` // drill speed: cycle duration is divided by it
|
||||||
Yield float64 `json:"yield"` // resources per stake
|
Yield float64 `json:"yield"` // resources per stake
|
||||||
@@ -38,8 +37,8 @@ func mult(v float64) float64 {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// bonusesFor combines the named advisors and attachments, capped.
|
// bonusesFor combines the named advisors, capped.
|
||||||
func (c *GameConfig) bonusesFor(advisors, attachments []string) Bonus {
|
func (c *GameConfig) bonusesFor(advisors []string) Bonus {
|
||||||
b := noBonus
|
b := noBonus
|
||||||
for _, name := range advisors {
|
for _, name := range advisors {
|
||||||
if a := c.advisor(name); a != nil {
|
if a := c.advisor(name); a != nil {
|
||||||
@@ -49,12 +48,6 @@ func (c *GameConfig) bonusesFor(advisors, attachments []string) Bonus {
|
|||||||
b.Refine *= mult(a.Refine)
|
b.Refine *= mult(a.Refine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, name := range attachments {
|
|
||||||
if m := c.attachment(name); m != nil {
|
|
||||||
b.Speed *= mult(m.Speed)
|
|
||||||
b.Yield *= mult(m.Yield)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Speed = min(b.Speed, c.BonusCaps.Speed)
|
b.Speed = min(b.Speed, c.BonusCaps.Speed)
|
||||||
b.Yield = min(b.Yield, c.BonusCaps.Yield)
|
b.Yield = min(b.Yield, c.BonusCaps.Yield)
|
||||||
b.Sell = min(b.Sell, c.BonusCaps.Sell)
|
b.Sell = min(b.Sell, c.BonusCaps.Sell)
|
||||||
@@ -62,10 +55,10 @@ func (c *GameConfig) bonusesFor(advisors, attachments []string) Bonus {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
// drillStats is a tool's cycle (seconds) and yield per stake with the player's bonus and
|
// drillStats is a tool's cycle (seconds, to a tenth, at least 1) and yield per stake with
|
||||||
// the live event multipliers. stakeInTx and the views both use it.
|
// the player's bonus and the live event multipliers. stakeInTx and the views both use it.
|
||||||
func drillStats(secs int, yield float64, b Bonus, eventSpeed, eventYield float64) (int, float64) {
|
func drillStats(secs, yield float64, b Bonus, eventSpeed, eventYield float64) (float64, float64) {
|
||||||
return max(1, int(math.Round(float64(secs)/(b.Speed*eventSpeed)))), yield * b.Yield * eventYield
|
return max(1, roundSecs(secs/(b.Speed*eventSpeed))), yield * b.Yield * eventYield
|
||||||
}
|
}
|
||||||
|
|
||||||
// pctText formats a multiplier as a bonus, e.g. 1.1 → "+10%".
|
// pctText formats a multiplier as a bonus, e.g. 1.1 → "+10%".
|
||||||
@@ -81,25 +74,12 @@ func (cfg *GameConfig) validateBonuses() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
okMult := func(v float64) bool { return v == 0 || v >= 1 }
|
okMult := func(v float64) bool { return v == 0 || v >= 1 }
|
||||||
if len(cfg.Attachments.Items) > 0 && cfg.Attachments.SlotsPerTool < 1 {
|
|
||||||
return fmt.Errorf("attachments: slotsPerTool must be at least 1")
|
|
||||||
}
|
|
||||||
names := map[string]bool{}
|
|
||||||
for _, m := range cfg.Attachments.Items {
|
|
||||||
if m.Name == "" || names[m.Name] {
|
|
||||||
return fmt.Errorf("attachments: names must be set and unique (%q)", m.Name)
|
|
||||||
}
|
|
||||||
names[m.Name] = true
|
|
||||||
if m.Price < 0 || !okMult(m.Speed) || !okMult(m.Yield) {
|
|
||||||
return fmt.Errorf("attachment %q: price can't be negative, speed and yield must be at least 1", m.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, p := range cfg.Advisors.SeatPrices {
|
for i, p := range cfg.Advisors.SeatPrices {
|
||||||
if p < 0 {
|
if p < 0 {
|
||||||
return fmt.Errorf("advisors: seat %d price is negative", i+2)
|
return fmt.Errorf("advisors: seat %d price is negative", i+2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
names = map[string]bool{}
|
names := map[string]bool{}
|
||||||
for _, a := range cfg.Advisors.Items {
|
for _, a := range cfg.Advisors.Items {
|
||||||
if a.Name == "" || names[a.Name] {
|
if a.Name == "" || names[a.Name] {
|
||||||
return fmt.Errorf("advisors: names must be set and unique (%q)", a.Name)
|
return fmt.Errorf("advisors: names must be set and unique (%q)", a.Name)
|
||||||
@@ -114,10 +94,9 @@ func (cfg *GameConfig) validateBonuses() error {
|
|||||||
|
|
||||||
// ------------------------------------------------------------------ in a transaction
|
// ------------------------------------------------------------------ in a transaction
|
||||||
|
|
||||||
// loadBonus reads the player's seated advisors and, with a tool id, the tool's attachments, and
|
// loadBonus reads the player's seated advisors and combines them. The advisor rows are
|
||||||
// combines them. Callers that pass a tool have locked its row; the advisor rows are
|
|
||||||
// share-locked so a hire can't change them halfway through the action.
|
// share-locked so a hire can't change them halfway through the action.
|
||||||
func loadBonus(ctx context.Context, tx pgx.Tx, userID, toolID string) (Bonus, error) {
|
func loadBonus(ctx context.Context, tx pgx.Tx, userID string) (Bonus, error) {
|
||||||
rows, err := tx.Query(ctx, `
|
rows, err := tx.Query(ctx, `
|
||||||
select advisor from advisor_hire where user_id = $1 and seat is not null order by advisor for share`, userID)
|
select advisor from advisor_hire where user_id = $1 and seat is not null order by advisor for share`, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -127,23 +106,12 @@ func loadBonus(ctx context.Context, tx pgx.Tx, userID, toolID string) (Bonus, er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return noBonus, err
|
return noBonus, err
|
||||||
}
|
}
|
||||||
var mods []string
|
return cfg().bonusesFor(advisors), nil
|
||||||
if toolID != "" {
|
|
||||||
rows, err = tx.Query(ctx, `
|
|
||||||
select item from attachment where inventory_item_id = $1 order by item`, toolID)
|
|
||||||
if err != nil {
|
|
||||||
return noBonus, err
|
|
||||||
}
|
|
||||||
if mods, err = pgx.CollectRows(rows, pgx.RowTo[string]); err != nil {
|
|
||||||
return noBonus, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cfg().bonusesFor(advisors, mods), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------ state
|
// ------------------------------------------------------------------ state
|
||||||
|
|
||||||
// loadBonusState loads the player's attachments, advisors (hired, and the seated ones) and
|
// loadBonusState loads the player's advisors (hired, and the seated ones) and
|
||||||
// advisor seats into the state.
|
// advisor seats into the state.
|
||||||
func loadBonusState(ctx context.Context, db querier, s *State) error {
|
func loadBonusState(ctx context.Context, db querier, s *State) error {
|
||||||
err := db.QueryRow(ctx, "select advisor_seats from users where id = $1", s.UserID).Scan(&s.AdvisorSeats)
|
err := db.QueryRow(ctx, "select advisor_seats from users where id = $1", s.UserID).Scan(&s.AdvisorSeats)
|
||||||
@@ -165,36 +133,20 @@ func loadBonusState(ctx context.Context, db querier, s *State) error {
|
|||||||
s.Advisors = append(s.Advisors, h.Name)
|
s.Advisors = append(s.Advisors, h.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rows, err = db.Query(ctx, `
|
|
||||||
select coalesce(inventory_item_id::text, ''), item from attachment
|
|
||||||
where user_id = $1 order by item`, s.UserID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.Fitted, s.Spare = map[string][]string{}, map[string]int{}
|
|
||||||
var toolID, item string
|
|
||||||
_, err = pgx.ForEachRow(rows, []any{&toolID, &item}, func() error {
|
|
||||||
if toolID == "" {
|
|
||||||
s.Spare[item]++
|
|
||||||
} else {
|
|
||||||
s.Fitted[toolID] = append(s.Fitted[toolID], item)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bonus is the player's advisor bonus (no attachments).
|
// Bonus is the player's advisor bonus.
|
||||||
func (s *State) Bonus() Bonus { return cfg().bonusesFor(s.Advisors, nil) }
|
func (s *State) Bonus() Bonus { return cfg().bonusesFor(s.Advisors) }
|
||||||
|
|
||||||
// toolBonus is the bonus for stakes of one tool: advisors and its attachments.
|
|
||||||
func (s *State) toolBonus(toolID string) Bonus {
|
|
||||||
return cfg().bonusesFor(s.Advisors, s.Fitted[toolID])
|
|
||||||
}
|
|
||||||
|
|
||||||
// drill is what a stake of the tool on a well of the resource would take and return now.
|
// drill is what a stake of the tool on a well of the resource would take and return now.
|
||||||
func (s *State) drill(t Tool, item *StoreItem, resource string) (int, float64) {
|
func (s *State) drill(t Tool, item *StoreItem, resource string) (float64, float64) {
|
||||||
return drillStats(item.ToolSecs(t.Tier), item.ToolYield(t.Tier), s.toolBonus(t.ID),
|
return s.drillAt(item, t.Levels(), resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
// drillAt is what a stake of the item at the levels would take and return now.
|
||||||
|
func (s *State) drillAt(item *StoreItem, lv Levels, resource string) (float64, float64) {
|
||||||
|
return drillStats(item.ToolSecs(lv), item.ToolYield(lv), s.Bonus(),
|
||||||
s.CycleBoosts.For("all"), s.YieldBoosts.For(resource))
|
s.CycleBoosts.For("all"), s.YieldBoosts.For(resource))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-36
@@ -1,6 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
// The game config (constants, store items and their upgrades, moon types) lives in
|
// The game config (constants, store items and their upgrade tracks, moon types) lives in
|
||||||
// config/game.json, embedded in the binary (`game`). Admins can override its numbers live
|
// config/game.json, embedded in the binary (`game`). Admins can override its numbers live
|
||||||
// (economy.go); read the effective config with cfg(). Store items are also synced into
|
// (economy.go); read the effective config with cfg(). Store items are also synced into
|
||||||
// the database (syncStoreItems), because inventory_item references store_item and the
|
// the database (syncStoreItems), because inventory_item references store_item and the
|
||||||
@@ -31,7 +31,7 @@ type GameConfig struct {
|
|||||||
MoonGrades []MoonGrade `json:"moonGrades"`
|
MoonGrades []MoonGrade `json:"moonGrades"`
|
||||||
// Selling a moon back (resale.go).
|
// Selling a moon back (resale.go).
|
||||||
MoonResale MoonResale `json:"moonResale"`
|
MoonResale MoonResale `json:"moonResale"`
|
||||||
// Selling tools and attachments back (resale.go).
|
// Selling tools back (resale.go).
|
||||||
EquipmentResale EquipmentResale `json:"equipmentResale"`
|
EquipmentResale EquipmentResale `json:"equipmentResale"`
|
||||||
// Credits per unit sold, by resource name.
|
// Credits per unit sold, by resource name.
|
||||||
ResourcePrices map[string]float64 `json:"resourcePrices"`
|
ResourcePrices map[string]float64 `json:"resourcePrices"`
|
||||||
@@ -45,9 +45,8 @@ type GameConfig struct {
|
|||||||
MoonNames MoonNames `json:"moonNames"`
|
MoonNames MoonNames `json:"moonNames"`
|
||||||
// Crafting refined items from resources (refinery.go).
|
// Crafting refined items from resources (refinery.go).
|
||||||
Refinery RefineryConfig `json:"refinery"`
|
Refinery RefineryConfig `json:"refinery"`
|
||||||
// Drill attachments and advisors (bonus.go): their combined bonuses are capped here.
|
// Advisors (bonus.go): their combined bonuses are capped here.
|
||||||
BonusCaps BonusCaps `json:"bonusCaps"`
|
BonusCaps BonusCaps `json:"bonusCaps"`
|
||||||
Attachments AttachmentConfig `json:"attachments"`
|
|
||||||
Advisors AdvisorConfig `json:"advisors"`
|
Advisors AdvisorConfig `json:"advisors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,23 +176,13 @@ func (cfg *GameConfig) validate() error {
|
|||||||
if item.Reach < 0 || item.Reach > len(cfg.ResourcePrices) {
|
if item.Reach < 0 || item.Reach > len(cfg.ResourcePrices) {
|
||||||
return fmt.Errorf("store item %d: reach must be 0 to %d wells", item.ID, len(cfg.ResourcePrices))
|
return fmt.Errorf("store item %d: reach must be 0 to %d wells", item.ID, len(cfg.ResourcePrices))
|
||||||
}
|
}
|
||||||
for _, u := range item.Upgrades {
|
if err := cfg.validateTracks(item); err != nil {
|
||||||
if u.Price < 0 {
|
return err
|
||||||
return fmt.Errorf("store item %d tier %d: price is negative", item.ID, u.Tier)
|
|
||||||
}
|
|
||||||
if u.Reach < 0 || u.Reach > len(cfg.ResourcePrices) {
|
|
||||||
return fmt.Errorf("store item %d tier %d: reach must be 0 to %d wells", item.ID, u.Tier, len(cfg.ResourcePrices))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ids[item.ID] {
|
if ids[item.ID] {
|
||||||
return fmt.Errorf("duplicate store item id %d", item.ID)
|
return fmt.Errorf("duplicate store item id %d", item.ID)
|
||||||
}
|
}
|
||||||
ids[item.ID] = true
|
ids[item.ID] = true
|
||||||
for i, u := range item.Upgrades {
|
|
||||||
if u.Tier != i+1 {
|
|
||||||
return fmt.Errorf("store item %d: upgrade tiers must be 1, 2, 3, ... in order", item.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for _, id := range cfg.StartingTools {
|
for _, id := range cfg.StartingTools {
|
||||||
if !ids[id] {
|
if !ids[id] {
|
||||||
@@ -281,7 +270,8 @@ func (cfg *GameConfig) validateWells() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// syncStoreItems makes store_item and upgrade_item match the effective config. Store items
|
// syncStoreItems makes store_item match the effective config (the upgrade tracks are read
|
||||||
|
// from the config, not stored). Store items
|
||||||
// that were removed from the file are deleted only if no player owns one (deleting
|
// that were removed from the file are deleted only if no player owns one (deleting
|
||||||
// would cascade to their tools).
|
// would cascade to their tools).
|
||||||
func syncStoreItems(ctx context.Context, db *pgxpool.Pool) error {
|
func syncStoreItems(ctx context.Context, db *pgxpool.Pool) error {
|
||||||
@@ -312,25 +302,6 @@ func syncStoreItemsTx(ctx context.Context, tx pgx.Tx, items []StoreItem) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("store item %d: %w", item.ID, err)
|
return fmt.Errorf("store item %d: %w", item.ID, err)
|
||||||
}
|
}
|
||||||
tiers := []int{}
|
|
||||||
for _, u := range item.Upgrades {
|
|
||||||
tiers = append(tiers, u.Tier)
|
|
||||||
_, err := tx.Exec(ctx, `
|
|
||||||
insert into upgrade_item(store_item_id, tier, price, claim_boost)
|
|
||||||
values ($1, $2, $3, $4)
|
|
||||||
on conflict (store_item_id, tier) do update set
|
|
||||||
price = excluded.price, claim_boost = excluded.claim_boost`,
|
|
||||||
item.ID, u.Tier, u.Price, u.ClaimBoost)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("store item %d tier %d: %w", item.ID, u.Tier, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(ctx, `
|
|
||||||
delete from upgrade_item where store_item_id = $1 and not (tier = any($2))`,
|
|
||||||
item.ID, tiers)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("store item %d: %w", item.ID, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := tx.Exec(ctx, `
|
_, err := tx.Exec(ctx, `
|
||||||
|
|||||||
+124
-75
@@ -32,115 +32,177 @@
|
|||||||
"description": "A hovering scout drone that skims ore off the surface.",
|
"description": "A hovering scout drone that skims ore off the surface.",
|
||||||
"image": "shovel.png",
|
"image": "shovel.png",
|
||||||
"price": 1200,
|
"price": 1200,
|
||||||
"claimAmount": 4,
|
"claimAmount": 8,
|
||||||
"completionTimeInSecs": 3,
|
"completionTimeInSecs": 3,
|
||||||
"upgrades": [
|
"tracks": {
|
||||||
{"tier": 1, "price": 80, "claimBoost": 0, "speed": 1.12},
|
"yield": {"name": "Twin rotors", "levels": [
|
||||||
{"tier": 2, "price": 100, "claimBoost": 0, "speed": 1.25},
|
{"value": 10, "price": 280},
|
||||||
{"tier": 3, "price": 80, "claimBoost": 0, "speed": 1.4, "parts": {"Dusk Alloy": 1}},
|
{"value": 12, "price": 280},
|
||||||
{"tier": 4, "price": 110, "claimBoost": 0, "speed": 1.6, "parts": {"Dusk Alloy": 1}},
|
{"value": 14, "price": 290, "parts": {"Dusk Alloy": 1}},
|
||||||
{"tier": 5, "price": 120, "claimBoost": 0, "speed": 1.8, "parts": {"Dusk Alloy": 2}}
|
{"value": 16, "price": 280, "parts": {"Dusk Alloy": 2}}
|
||||||
]
|
]},
|
||||||
|
"cycle": {"name": "Flight computer", "levels": [
|
||||||
|
{"value": 2.6, "price": 180},
|
||||||
|
{"value": 2.2, "price": 240},
|
||||||
|
{"value": 1.9, "price": 250, "parts": {"Dusk Alloy": 1}},
|
||||||
|
{"value": 1.7, "price": 210, "parts": {"Dusk Alloy": 1}},
|
||||||
|
{"value": 1.5, "price": 270, "parts": {"Dusk Alloy": 2}}
|
||||||
|
]}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"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": 5700,
|
"price": 4000,
|
||||||
"claimAmount": 20,
|
"claimAmount": 60,
|
||||||
"completionTimeInSecs": 6,
|
"completionTimeInSecs": 6,
|
||||||
"upgrades": [
|
"tracks": {
|
||||||
{"tier": 1, "price": 400, "claimBoost": 0, "speed": 1.12},
|
"yield": {"name": "Wider chisel", "levels": [
|
||||||
{"tier": 2, "price": 470, "claimBoost": 0, "speed": 1.25},
|
{"value": 72, "price": 760},
|
||||||
{"tier": 3, "price": 500, "claimBoost": 0, "speed": 1.4, "parts": {"Dusk Alloy": 2}},
|
{"value": 84, "price": 760},
|
||||||
{"tier": 4, "price": 610, "claimBoost": 0, "speed": 1.6, "parts": {"Cryo Cell": 1}},
|
{"value": 96, "price": 760, "parts": {"Dusk Alloy": 2}},
|
||||||
{"tier": 5, "price": 780, "claimBoost": 0, "speed": 1.8, "parts": {"Plasma Weave": 1}}
|
{"value": 108, "price": 760, "parts": {"Cryo Cell": 1}},
|
||||||
]
|
{"value": 120, "price": 760, "parts": {"Plasma Weave": 1}}
|
||||||
|
]},
|
||||||
|
"cycle": {"name": "Pneumatic hammer", "levels": [
|
||||||
|
{"value": 5.2, "price": 580},
|
||||||
|
{"value": 4.5, "price": 680},
|
||||||
|
{"value": 3.9, "price": 780, "parts": {"Dusk Alloy": 2}},
|
||||||
|
{"value": 3.4, "price": 860, "parts": {"Cryo Cell": 1}},
|
||||||
|
{"value": 3, "price": 890, "parts": {"Plasma Weave": 1}}
|
||||||
|
]}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"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": 27000,
|
"price": 13400,
|
||||||
"claimAmount": 110,
|
"claimAmount": 300,
|
||||||
"completionTimeInSecs": 8,
|
"completionTimeInSecs": 8,
|
||||||
"upgrades": [
|
"tracks": {
|
||||||
{"tier": 1, "price": 1700, "claimBoost": 0, "speed": 1.12},
|
"yield": {"name": "Wide-bore bit", "levels": [
|
||||||
{"tier": 2, "price": 4500, "claimBoost": 0, "speed": 1.25},
|
{"value": 355, "price": 2300},
|
||||||
{"tier": 3, "price": 2900, "claimBoost": 0, "speed": 1.4, "parts": {"Cryo Cell": 1}},
|
{"value": 423, "price": 2900},
|
||||||
{"tier": 4, "price": 3400, "claimBoost": 0, "speed": 1.6, "parts": {"Plasma Weave": 2}},
|
{"value": 477, "price": 2300, "parts": {"Cryo Cell": 1}},
|
||||||
{"tier": 5, "price": 4300, "claimBoost": 0, "speed": 1.8, "parts": {"Plasma Weave": 3}}
|
{"value": 545, "price": 2900, "parts": {"Plasma Weave": 2}},
|
||||||
]
|
{"value": 600, "price": 2300, "parts": {"Plasma Weave": 3}}
|
||||||
|
]},
|
||||||
|
"cycle": {"name": "Spindle motor", "levels": [
|
||||||
|
{"value": 7, "price": 1800},
|
||||||
|
{"value": 6, "price": 2400},
|
||||||
|
{"value": 5.2, "price": 2600, "parts": {"Cryo Cell": 1}},
|
||||||
|
{"value": 4.6, "price": 2600, "parts": {"Plasma Weave": 2}},
|
||||||
|
{"value": 4, "price": 3300, "parts": {"Plasma Weave": 3}}
|
||||||
|
]}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
"id": 4,
|
||||||
"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": 290000,
|
"price": 129000,
|
||||||
"claimAmount": 1350,
|
"claimAmount": 4000,
|
||||||
"completionTimeInSecs": 10,
|
"completionTimeInSecs": 10,
|
||||||
"reach": 2,
|
"reach": 2,
|
||||||
"upgrades": [
|
"tracks": {
|
||||||
{"tier": 1, "price": 37000, "claimBoost": 0, "speed": 1.12},
|
"yield": {"name": "Wider beam", "levels": [
|
||||||
{"tier": 2, "price": 23000, "claimBoost": 0, "speed": 1.25},
|
{"value": 4740, "price": 23000},
|
||||||
{"tier": 3, "price": 28000, "claimBoost": 0, "speed": 1.4, "parts": {"Cryo Cell": 2}},
|
{"value": 5630, "price": 27000},
|
||||||
{"tier": 4, "price": 34000, "claimBoost": 0, "speed": 1.6, "parts": {"Plasma Weave": 3}},
|
{"value": 6370, "price": 23000, "parts": {"Cryo Cell": 2}},
|
||||||
{"tier": 5, "price": 42000, "claimBoost": 0, "speed": 1.8, "parts": {"Void Core": 1}}
|
{"value": 7260, "price": 27000, "parts": {"Plasma Weave": 3}},
|
||||||
]
|
{"value": 8000, "price": 23000, "parts": {"Void Core": 1}}
|
||||||
|
]},
|
||||||
|
"cycle": {"name": "Laser capacitors", "levels": [
|
||||||
|
{"value": 8.7, "price": 18000},
|
||||||
|
{"value": 7.5, "price": 23000},
|
||||||
|
{"value": 6.5, "price": 25000, "parts": {"Cryo Cell": 2}},
|
||||||
|
{"value": 5.7, "price": 26000, "parts": {"Plasma Weave": 3}},
|
||||||
|
{"value": 5, "price": 30000, "parts": {"Void Core": 1}}
|
||||||
|
]}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
"id": 5,
|
||||||
"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": 3200000,
|
"price": 1180000,
|
||||||
"claimAmount": 17000,
|
"claimAmount": 48000,
|
||||||
"completionTimeInSecs": 12,
|
"completionTimeInSecs": 12,
|
||||||
"reach": 3,
|
"reach": 3,
|
||||||
"upgrades": [
|
"tracks": {
|
||||||
{"tier": 1, "price": 380000, "claimBoost": 0, "speed": 1.12},
|
"yield": {"name": "Fine sieves", "levels": [
|
||||||
{"tier": 2, "price": 230000, "claimBoost": 0, "speed": 1.25},
|
{"value": 56470, "price": 200000},
|
||||||
{"tier": 3, "price": 280000, "claimBoost": 0, "speed": 1.4, "parts": {"Plasma Weave": 3}},
|
{"value": 67760, "price": 260000},
|
||||||
{"tier": 4, "price": 740000, "claimBoost": 0, "speed": 1.6, "parts": {"Void Core": 1}},
|
{"value": 76240, "price": 200000, "parts": {"Plasma Weave": 3}},
|
||||||
{"tier": 5, "price": 510000, "claimBoost": 0, "speed": 1.8, "parts": {"Stellar Lattice": 1}}
|
{"value": 87530, "price": 260000, "parts": {"Void Core": 1}},
|
||||||
]
|
{"value": 96000, "price": 200000, "parts": {"Stellar Lattice": 1}}
|
||||||
|
]},
|
||||||
|
"cycle": {"name": "Auger gearbox", "levels": [
|
||||||
|
{"value": 10.4, "price": 170000},
|
||||||
|
{"value": 9, "price": 200000},
|
||||||
|
{"value": 7.8, "price": 230000, "parts": {"Plasma Weave": 3}},
|
||||||
|
{"value": 6.8, "price": 250000, "parts": {"Void Core": 1}},
|
||||||
|
{"value": 6, "price": 260000, "parts": {"Stellar Lattice": 1}}
|
||||||
|
]}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 6,
|
"id": 6,
|
||||||
"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": 7400000,
|
"price": 2120000,
|
||||||
"claimAmount": 36000,
|
"claimAmount": 110000,
|
||||||
"completionTimeInSecs": 14,
|
"completionTimeInSecs": 14,
|
||||||
"reach": 5,
|
"reach": 5,
|
||||||
"upgrades": [
|
"tracks": {
|
||||||
{"tier": 1, "price": 470000, "claimBoost": 0, "speed": 1.12},
|
"yield": {"name": "Larger plates", "levels": [
|
||||||
{"tier": 2, "price": 1200000, "claimBoost": 0, "speed": 1.25},
|
{"value": 131390, "price": 390000},
|
||||||
{"tier": 3, "price": 820000, "claimBoost": 0, "speed": 1.4, "parts": {"Void Core": 1}},
|
{"value": 152780, "price": 390000},
|
||||||
{"tier": 4, "price": 1000000, "claimBoost": 0, "speed": 1.6, "parts": {"Stellar Lattice": 1}},
|
{"value": 177220, "price": 450000, "parts": {"Void Core": 1}},
|
||||||
{"tier": 5, "price": 1300000, "claimBoost": 0, "speed": 1.8, "parts": {"Void Core": 2, "Stellar Lattice": 2}}
|
{"value": 198610, "price": 390000, "parts": {"Stellar Lattice": 1}},
|
||||||
]
|
{"value": 220000, "price": 390000, "parts": {"Void Core": 2, "Stellar Lattice": 2}}
|
||||||
|
]},
|
||||||
|
"cycle": {"name": "Charge coils", "levels": [
|
||||||
|
{"value": 12.2, "price": 300000},
|
||||||
|
{"value": 10.5, "price": 370000},
|
||||||
|
{"value": 9.1, "price": 410000, "parts": {"Void Core": 1}},
|
||||||
|
{"value": 8, "price": 430000, "parts": {"Stellar Lattice": 1}},
|
||||||
|
{"value": 7, "price": 500000, "parts": {"Void Core": 2, "Stellar Lattice": 2}}
|
||||||
|
]}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 7,
|
"id": 7,
|
||||||
"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": 11000000,
|
"price": 3100000,
|
||||||
"claimAmount": 47000,
|
"claimAmount": 200000,
|
||||||
"completionTimeInSecs": 16,
|
"completionTimeInSecs": 16,
|
||||||
"reach": 5,
|
"reach": 5,
|
||||||
"upgrades": [
|
"tracks": {
|
||||||
{"tier": 1, "price": 840000, "claimBoost": 0, "speed": 1.12},
|
"yield": {"name": "Core barrel", "levels": [
|
||||||
{"tier": 2, "price": 1000000, "claimBoost": 0, "speed": 1.25},
|
{"value": 238300, "price": 560000},
|
||||||
{"tier": 3, "price": 1300000, "claimBoost": 0, "speed": 1.4, "parts": {"Void Core": 2}},
|
{"value": 280850, "price": 630000},
|
||||||
{"tier": 4, "price": 1600000, "claimBoost": 0, "speed": 1.6, "parts": {"Stellar Lattice": 2}},
|
{"value": 319150, "price": 560000, "parts": {"Void Core": 2}},
|
||||||
{"tier": 5, "price": 2200000, "claimBoost": 0, "speed": 1.8, "parts": {"Void Core": 3, "Stellar Lattice": 3}}
|
{"value": 361700, "price": 630000, "parts": {"Stellar Lattice": 2}},
|
||||||
]
|
{"value": 400000, "price": 560000, "parts": {"Void Core": 3, "Stellar Lattice": 3}}
|
||||||
|
]},
|
||||||
|
"cycle": {"name": "Drill string", "levels": [
|
||||||
|
{"value": 14, "price": 420000},
|
||||||
|
{"value": 12, "price": 560000},
|
||||||
|
{"value": 10.4, "price": 600000, "parts": {"Void Core": 2}},
|
||||||
|
{"value": 9.1, "price": 650000, "parts": {"Stellar Lattice": 2}},
|
||||||
|
{"value": 8, "price": 710000, "parts": {"Void Core": 3, "Stellar Lattice": 3}}
|
||||||
|
]}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"moons": [
|
"moons": [
|
||||||
@@ -225,19 +287,6 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"bonusCaps": {"speed": 1.6, "yield": 1.6, "sell": 1.15, "refine": 1.5},
|
"bonusCaps": {"speed": 1.6, "yield": 1.6, "sell": 1.15, "refine": 1.5},
|
||||||
"attachments": {
|
|
||||||
"slotsPerTool": 2,
|
|
||||||
"items": [
|
|
||||||
{"name": "Turbo Core", "image": "turbo-core.png", "price": 900, "speed": 1.2,
|
|
||||||
"description": "An overdriven motor core. Runs hot, cycles fast."},
|
|
||||||
{"name": "Diamond Bit", "image": "diamond-bit.png", "price": 950, "yield": 1.15,
|
|
||||||
"description": "A synthetic-diamond cutting face that keeps more ore in every bite."},
|
|
||||||
{"name": "Cryo Cooler", "image": "cryo-cooler.png", "price": 85000, "speed": 1.35,
|
|
||||||
"description": "Wraps the spindle in coolant so the drill never stops to cool down."},
|
|
||||||
{"name": "Seam Scanner", "image": "seam-scanner.png", "price": 90000, "yield": 1.3,
|
|
||||||
"description": "Ground radar that steers the head into the richest part of the seam."}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"advisors": {
|
"advisors": {
|
||||||
"seatPrices": [20000, 400000, 8000000],
|
"seatPrices": [20000, 400000, 8000000],
|
||||||
"items": [
|
"items": [
|
||||||
|
|||||||
+73
-197
@@ -11,32 +11,16 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"maps"
|
"maps"
|
||||||
"math"
|
"math"
|
||||||
mrand "math/rand/v2"
|
mrand "math/rand/v2"
|
||||||
"path"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Upgrade struct {
|
|
||||||
Tier int `json:"tier"`
|
|
||||||
Price float64 `json:"price"`
|
|
||||||
ClaimBoost float64 `json:"claimBoost"`
|
|
||||||
// Refined items (by name) used up by this upgrade, besides the price (refinery.go).
|
|
||||||
Parts map[string]float64 `json:"parts,omitempty"`
|
|
||||||
// Wells mined per cycle from this tier on (see StoreItem.Reach); 0 keeps the tool's.
|
|
||||||
Reach int `json:"reach,omitempty"`
|
|
||||||
// Cycle speed multiplier at this tier (1.25: cycles take 1/1.25 as long); 0 or 1: none.
|
|
||||||
// Like claimBoost it is the tier's total, not added to the tiers below. Read from cfg().
|
|
||||||
Speed float64 `json:"speed,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StoreItem struct {
|
type StoreItem struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -47,9 +31,10 @@ type StoreItem struct {
|
|||||||
CompletionTimeInSecs int `json:"completionTimeInSecs"`
|
CompletionTimeInSecs int `json:"completionTimeInSecs"`
|
||||||
// How many wells of a moon one cycle mines (0 or 1: the one it's deployed on). A tool
|
// How many wells of a moon one cycle mines (0 or 1: the one it's deployed on). A tool
|
||||||
// with a reach as big as a moon's well count (len(resourcePrices)) strips the whole moon.
|
// with a reach as big as a moon's well count (len(resourcePrices)) strips the whole moon.
|
||||||
// Read from cfg() like upgrade parts, not stored in store_item.
|
// Read from cfg() like the tracks, not stored in store_item.
|
||||||
Reach int `json:"reach,omitempty"`
|
Reach int `json:"reach,omitempty"`
|
||||||
Upgrades []Upgrade `json:"upgrades"`
|
// The two upgrade tracks, yield and cycle (tracks.go). Read from cfg().
|
||||||
|
Tracks Tracks `json:"tracks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config is the part of the game config the UI needs. Store items come from the
|
// Config is the part of the game config the UI needs. Store items come from the
|
||||||
@@ -90,7 +75,8 @@ type Moon struct {
|
|||||||
type Tool struct {
|
type Tool struct {
|
||||||
ID string
|
ID string
|
||||||
StoreItemID int
|
StoreItemID int
|
||||||
Tier int
|
YieldLevel int // levels on its upgrade tracks (tracks.go)
|
||||||
|
CycleLevel int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stake is an unclaimed staking event: a tool mining a well.
|
// Stake is an unclaimed staking event: a tool mining a well.
|
||||||
@@ -102,12 +88,12 @@ type Stake struct {
|
|||||||
Resource string
|
Resource string
|
||||||
Amount float64
|
Amount float64
|
||||||
Start time.Time
|
Start time.Time
|
||||||
DurationInSecs int
|
DurationInSecs float64
|
||||||
Reach int // wells of the moon it mines (StoreItem.Reach); WellID is the first
|
Reach int // wells of the moon it mines (StoreItem.Reach); WellID is the first
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Stake) End() time.Time {
|
func (s Stake) End() time.Time {
|
||||||
return s.Start.Add(time.Duration(s.DurationInSecs) * time.Second)
|
return s.Start.Add(time.Duration(s.DurationInSecs * float64(time.Second)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Stake) Remaining(now time.Time) time.Duration {
|
func (s Stake) Remaining(now time.Time) time.Duration {
|
||||||
@@ -144,11 +130,8 @@ type State struct {
|
|||||||
YieldBoosts PriceBoosts // active live yield events
|
YieldBoosts PriceBoosts // active live yield events
|
||||||
Pressure map[string]float64 // the player's selling pressure by item, now (market.go)
|
Pressure map[string]float64 // the player's selling pressure by item, now (market.go)
|
||||||
|
|
||||||
// Bonuses (bonus.go): attachments equipped on each tool and spare ones (by kind),
|
// Bonuses (bonus.go): seated advisors (their bonus counts), every advisor hired with
|
||||||
// seated advisors (their bonus counts), every advisor hired with their seat, advisor
|
// their seat, advisor seats owned.
|
||||||
// seats owned.
|
|
||||||
Fitted map[string][]string
|
|
||||||
Spare map[string]int
|
|
||||||
Advisors []string
|
Advisors []string
|
||||||
Hires []AdvisorHire // seated first, by seat
|
Hires []AdvisorHire // seated first, by seat
|
||||||
AdvisorSeats int
|
AdvisorSeats int
|
||||||
@@ -190,60 +173,6 @@ func (s *State) StakeForTool(toolID string) *Stake {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToolYield is what one completed stake returns at the given tier.
|
|
||||||
func (item *StoreItem) ToolYield(tier int) float64 {
|
|
||||||
for _, u := range item.Upgrades {
|
|
||||||
if u.Tier == tier {
|
|
||||||
return item.ClaimAmount + u.ClaimBoost
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item.ClaimAmount
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToolSecs is a stake's cycle at the given tier, before bonuses: the tool's cycle over the
|
|
||||||
// tier's speed.
|
|
||||||
func (item *StoreItem) ToolSecs(tier int) int {
|
|
||||||
if u := item.upgrade(tier); u != nil && u.Speed > 1 {
|
|
||||||
return max(1, int(math.Round(float64(item.CompletionTimeInSecs)/u.Speed)))
|
|
||||||
}
|
|
||||||
return item.CompletionTimeInSecs
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToolReach is how many wells a stake mines at the given tier: the reach of the highest
|
|
||||||
// tier up to it that sets one, else the tool's, at least 1.
|
|
||||||
func (item *StoreItem) ToolReach(tier int) int {
|
|
||||||
reach := item.Reach
|
|
||||||
for _, u := range item.Upgrades {
|
|
||||||
if u.Tier <= tier && u.Reach > 0 {
|
|
||||||
reach = u.Reach
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return max(1, reach)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImageAt is the tool's image at a tier: tier N is the image's name with "-tN" before the
|
|
||||||
// extension (drawn by scripts/tool-art.mjs), else the tier 0 image when that file is missing.
|
|
||||||
func (item *StoreItem) ImageAt(tier int) string {
|
|
||||||
if tier <= 0 {
|
|
||||||
return item.Image
|
|
||||||
}
|
|
||||||
ext := path.Ext(item.Image)
|
|
||||||
name := fmt.Sprintf("%s-t%d%s", strings.TrimSuffix(item.Image, ext), tier, ext)
|
|
||||||
if _, err := fs.Stat(embedded, "web/static/assets/items/"+name); err != nil {
|
|
||||||
return item.Image
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (item *StoreItem) NextUpgrade(tier int) *Upgrade {
|
|
||||||
for i := range item.Upgrades {
|
|
||||||
if item.Upgrades[i].Tier == tier+1 {
|
|
||||||
return &item.Upgrades[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------ reads
|
// ------------------------------------------------------------------ reads
|
||||||
|
|
||||||
func loadConfig(ctx context.Context, db *pgxpool.Pool) (Config, error) {
|
func loadConfig(ctx context.Context, db *pgxpool.Pool) (Config, error) {
|
||||||
@@ -274,33 +203,11 @@ func loadConfig(ctx context.Context, db *pgxpool.Pool) (Config, error) {
|
|||||||
return cfg, err
|
return cfg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err = db.Query(ctx, `
|
// Reach and the upgrade tracks aren't in the tables: they come from the game config.
|
||||||
select store_item_id, tier, price, claim_boost from upgrade_item
|
|
||||||
order by store_item_id, tier`)
|
|
||||||
if err != nil {
|
|
||||||
return cfg, err
|
|
||||||
}
|
|
||||||
var itemID int
|
|
||||||
var u Upgrade
|
|
||||||
_, err = pgx.ForEachRow(rows, []any{&itemID, &u.Tier, &u.Price, &u.ClaimBoost}, func() error {
|
|
||||||
for i := range cfg.StoreItems {
|
|
||||||
if cfg.StoreItems[i].ID == itemID {
|
|
||||||
cfg.StoreItems[i].Upgrades = append(cfg.StoreItems[i].Upgrades, u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
// Reach and upgrade speed aren't in the tables: they come from the game config.
|
|
||||||
for i := range cfg.StoreItems {
|
for i := range cfg.StoreItems {
|
||||||
item := &cfg.StoreItems[i]
|
item := &cfg.StoreItems[i]
|
||||||
if c := configItem(item.ID); c != nil {
|
if c := configItem(item.ID); c != nil {
|
||||||
item.Reach = c.Reach
|
item.Reach, item.Tracks = c.Reach, c.Tracks
|
||||||
for j := range item.Upgrades {
|
|
||||||
if up := c.upgrade(item.Upgrades[j].Tier); up != nil {
|
|
||||||
item.Upgrades[j].Reach = up.Reach
|
|
||||||
item.Upgrades[j].Speed = up.Speed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cfg, err
|
return cfg, err
|
||||||
@@ -317,19 +224,10 @@ func configItem(id int) *StoreItem {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (item *StoreItem) upgrade(tier int) *Upgrade {
|
// toolReach is how many wells a stake of the store item mines (game config).
|
||||||
for i := range item.Upgrades {
|
func toolReach(storeItemID int) int {
|
||||||
if item.Upgrades[i].Tier == tier {
|
|
||||||
return &item.Upgrades[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// toolReach is how many wells a stake of the store item at the tier mines (game config).
|
|
||||||
func toolReach(storeItemID, tier int) int {
|
|
||||||
if item := configItem(storeItemID); item != nil {
|
if item := configItem(storeItemID); item != nil {
|
||||||
return item.ToolReach(tier)
|
return item.ToolReach()
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -414,7 +312,7 @@ func loadState(ctx context.Context, db *pgxpool.Pool, userID string) (*State, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
rows, err = db.Query(ctx, `
|
rows, err = db.Query(ctx, `
|
||||||
select id, store_item_id, tier from inventory_item
|
select id, store_item_id, yield_level, cycle_level from inventory_item
|
||||||
where user_id = $1 order by store_item_id, created_at, id`, userID)
|
where user_id = $1 order by store_item_id, created_at, id`, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -951,51 +849,6 @@ func buyTool(ctx context.Context, db *pgxpool.Pool, userID string, storeItemID i
|
|||||||
return toolID, err
|
return toolID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// upgradeTool upgrades a tool (an idle copy of it, if there is one: see pickTool) and
|
|
||||||
// returns the one it upgraded.
|
|
||||||
func upgradeTool(ctx context.Context, db *pgxpool.Pool, userID, toolID string) (string, error) {
|
|
||||||
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
|
||||||
var err error
|
|
||||||
if toolID, err = pickTool(ctx, tx, userID, toolID, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var storeItemID, tier int
|
|
||||||
err = tx.QueryRow(ctx, "select store_item_id, tier from inventory_item where id = $1",
|
|
||||||
toolID).Scan(&storeItemID, &tier)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var price float64
|
|
||||||
err = tx.QueryRow(ctx, `
|
|
||||||
select price from upgrade_item where store_item_id = $1 and tier = $2`,
|
|
||||||
storeItemID, tier+1).Scan(&price)
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return gameErr("This tool is already at its highest tier")
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := debit(ctx, tx, userID, price); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := takeItems(ctx, tx, userID, upgradeParts(storeItemID, tier+1)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec(ctx, "update inventory_item set tier = tier + 1 where id = $1", toolID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(ctx, "insert into upgrade_event(inventory_item_id) values ($1)", toolID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return logEvent(ctx, tx, userID, "upgrade",
|
|
||||||
map[string]any{"item": storeItemID, "tier": tier + 1, "price": price})
|
|
||||||
})
|
|
||||||
return toolID, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// stake puts an idle tool to work on one of the player's wells. If the tool is busy (say
|
// stake puts an idle tool to work on one of the player's wells. If the tool is busy (say
|
||||||
// the page was stale), another idle tool of the same type goes instead.
|
// the page was stale), another idle tool of the same type goes instead.
|
||||||
func stake(ctx context.Context, db *pgxpool.Pool, userID, toolID, wellID string) error {
|
func stake(ctx context.Context, db *pgxpool.Pool, userID, toolID, wellID string) error {
|
||||||
@@ -1008,20 +861,18 @@ func stake(ctx context.Context, db *pgxpool.Pool, userID, toolID, wellID string)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// pickTool locks the tool an action on toolID should use. Copies of a tool (same type,
|
// pickTool locks the tool an action on toolID should use. Copies of a tool (same type and
|
||||||
// tier and attachments) are interchangeable, so when toolID is busy (an unclaimed stake)
|
// track levels) are interchangeable, so when toolID is busy (an unclaimed stake) an idle
|
||||||
// an idle copy is used instead; with anyTier, any idle tool of the same type, the best
|
// copy is used instead; with anyLevel, any idle tool of the same type, the best one first
|
||||||
// one first (a copy, then highest tier, most attachments). Without an idle one it's
|
// (a copy, then the most levels). Without an idle one it's toolID itself, and the action
|
||||||
// toolID itself, and the action reports why it can't use it.
|
// reports why it can't use it.
|
||||||
func pickTool(ctx context.Context, tx pgx.Tx, userID, toolID string, anyTier bool) (string, error) {
|
func pickTool(ctx context.Context, tx pgx.Tx, userID, toolID string, anyLevel bool) (string, error) {
|
||||||
if !uuidPattern.MatchString(toolID) {
|
if !uuidPattern.MatchString(toolID) {
|
||||||
return "", gameErr("Tool not found")
|
return "", gameErr("Tool not found")
|
||||||
}
|
}
|
||||||
rows, err := tx.Query(ctx, `
|
rows, err := tx.Query(ctx, `
|
||||||
with tool as (
|
with tool as (
|
||||||
select inventory_item.id, store_item_id, tier,
|
select inventory_item.id, store_item_id, yield_level, cycle_level,
|
||||||
array(select item from attachment where inventory_item_id = inventory_item.id
|
|
||||||
order by item) as gear,
|
|
||||||
exists(select 1 from staking_event
|
exists(select 1 from staking_event
|
||||||
where inventory_item_id = inventory_item.id
|
where inventory_item_id = inventory_item.id
|
||||||
and not exists (select 1 from claim_event
|
and not exists (select 1 from claim_event
|
||||||
@@ -1030,9 +881,10 @@ func pickTool(ctx context.Context, tx pgx.Tx, userID, toolID string, anyTier boo
|
|||||||
)
|
)
|
||||||
select tool.id from tool join tool me on me.id = $2
|
select tool.id from tool join tool me on me.id = $2
|
||||||
where tool.store_item_id = me.store_item_id and not tool.busy
|
where tool.store_item_id = me.store_item_id and not tool.busy
|
||||||
and ($3 or (tool.tier = me.tier and tool.gear = me.gear))
|
and ($3 or (tool.yield_level = me.yield_level and tool.cycle_level = me.cycle_level))
|
||||||
order by tool.id = me.id desc, (tool.tier = me.tier and tool.gear = me.gear) desc,
|
order by tool.id = me.id desc,
|
||||||
tool.tier desc, cardinality(tool.gear) desc, tool.id`, userID, toolID, anyTier)
|
(tool.yield_level = me.yield_level and tool.cycle_level = me.cycle_level) desc,
|
||||||
|
tool.yield_level + tool.cycle_level desc, tool.yield_level desc, tool.id`, userID, toolID, anyLevel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -1117,23 +969,21 @@ func stakeInTx(ctx context.Context, tx pgx.Tx, userID, toolID, wellID string) er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var duration, storeItemID, tier int
|
var storeItemID int
|
||||||
var amount float64
|
var lv Levels
|
||||||
err = tx.QueryRow(ctx, `
|
err = tx.QueryRow(ctx, `
|
||||||
select store_item.completion_time_in_secs,
|
select store_item_id, yield_level, cycle_level from inventory_item
|
||||||
store_item.claim_amount + coalesce(upgrade_item.claim_boost, 0),
|
where id = $1 and user_id = $2
|
||||||
store_item.id, inventory_item.tier
|
for update`, toolID, userID).Scan(&storeItemID, &lv.Yield, &lv.Cycle)
|
||||||
from inventory_item
|
|
||||||
join store_item on store_item.id = inventory_item.store_item_id
|
|
||||||
left join upgrade_item on upgrade_item.store_item_id = store_item.id
|
|
||||||
and upgrade_item.tier = inventory_item.tier
|
|
||||||
where inventory_item.id = $1 and inventory_item.user_id = $2
|
|
||||||
for update of inventory_item`, toolID, userID).Scan(&duration, &amount, &storeItemID, &tier)
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return gameErr("Tool not found")
|
return gameErr("Tool not found")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
item := configItem(storeItemID)
|
||||||
|
if item == nil {
|
||||||
|
return gameErr("Tool not found")
|
||||||
|
}
|
||||||
|
|
||||||
// A tool is busy until its stake is claimed.
|
// A tool is busy until its stake is claimed.
|
||||||
var busy bool
|
var busy bool
|
||||||
@@ -1165,7 +1015,7 @@ func stakeInTx(ctx context.Context, tx pgx.Tx, userID, toolID, wellID string) er
|
|||||||
}
|
}
|
||||||
// A tool that mines several wells (reach) is tapped out only when the whole moon is:
|
// A tool that mines several wells (reach) is tapped out only when the whole moon is:
|
||||||
// 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 := toolReach(storeItemID, tier)
|
reach := item.ToolReach()
|
||||||
if supply <= 0 && reach > 1 {
|
if supply <= 0 && reach > 1 {
|
||||||
err = tx.QueryRow(ctx, `
|
err = tx.QueryRow(ctx, `
|
||||||
select id, supply, (select name from resource where id = resource_id) from resource_well
|
select id, supply, (select name from resource where id = resource_id) from resource_well
|
||||||
@@ -1179,9 +1029,9 @@ func stakeInTx(ctx context.Context, tx pgx.Tx, userID, toolID, wellID string) er
|
|||||||
return errWellEmpty
|
return errWellEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attachments, advisors and live events (a cycle multiplier of 2 halves the stake's
|
// Advisors and live events (a cycle multiplier of 2 halves the stake's
|
||||||
// duration) count for the stakes started while they apply: fixed now, not at claim time.
|
// duration) count for the stakes started while they apply: fixed now, not at claim time.
|
||||||
bonus, err := loadBonus(ctx, tx, userID, toolID)
|
bonus, err := loadBonus(ctx, tx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1195,10 +1045,7 @@ func stakeInTx(ctx context.Context, tx pgx.Tx, userID, toolID, wellID string) er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if item := configItem(storeItemID); item != nil {
|
duration, amount := drillStats(item.ToolSecs(lv), item.ToolYield(lv), bonus, speed, yield)
|
||||||
duration = item.ToolSecs(tier)
|
|
||||||
}
|
|
||||||
duration, amount = drillStats(duration, amount, bonus, speed, yield)
|
|
||||||
|
|
||||||
// The drills already on the well (and multi-well ones reaching it) will take all it
|
// The drills already on the well (and multi-well ones reaching it) will take all it
|
||||||
// has left: this one would get nothing.
|
// has left: this one would get nothing.
|
||||||
@@ -1350,8 +1197,7 @@ func redrill(ctx context.Context, db *pgxpool.Pool, userID, wellID string) (int,
|
|||||||
var n int
|
var n int
|
||||||
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
||||||
rows, err := tx.Query(ctx, `
|
rows, err := tx.Query(ctx, `
|
||||||
select inventory_item.id, store_item.claim_amount from inventory_item
|
select inventory_item.id, store_item_id, yield_level, cycle_level from inventory_item
|
||||||
join store_item on store_item.id = inventory_item.store_item_id
|
|
||||||
join lateral (
|
join lateral (
|
||||||
select staking_event.id, well_id from staking_event
|
select staking_event.id, well_id from staking_event
|
||||||
where inventory_item_id = inventory_item.id
|
where inventory_item_id = inventory_item.id
|
||||||
@@ -1366,15 +1212,45 @@ func redrill(ctx context.Context, db *pgxpool.Pool, userID, wellID string) (int,
|
|||||||
}
|
}
|
||||||
type idle struct {
|
type idle struct {
|
||||||
ID string
|
ID string
|
||||||
Amount float64
|
StoreItemID int
|
||||||
|
Lv Levels
|
||||||
|
Amount float64 // what a stake of it takes here: its yield-track value, bonuses and events
|
||||||
}
|
}
|
||||||
tools, err := pgx.CollectRows(rows, pgx.RowToStructByPos[idle])
|
var tools []idle
|
||||||
|
var t idle
|
||||||
|
_, err = pgx.ForEachRow(rows, []any{&t.ID, &t.StoreItemID, &t.Lv.Yield, &t.Lv.Cycle}, func() error {
|
||||||
|
tools = append(tools, t)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(tools) == 0 {
|
if len(tools) == 0 {
|
||||||
return gameErr("No idle drills to redrill here")
|
return gameErr("No idle drills to redrill here")
|
||||||
}
|
}
|
||||||
|
bonus, err := loadBonus(ctx, tx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var resource string
|
||||||
|
err = tx.QueryRow(ctx, `select resource.name from resource_well join resource on resource.id = resource_id
|
||||||
|
where resource_well.id = $1`, wellID).Scan(&resource)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
speed, err := eventMultiplier(ctx, tx, "cycle_multiplier", "all")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
yield, err := eventMultiplier(ctx, tx, "yield_multiplier", resource)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := range tools {
|
||||||
|
if item := configItem(tools[i].StoreItemID); item != nil {
|
||||||
|
_, tools[i].Amount = drillStats(item.ToolSecs(tools[i].Lv), item.ToolYield(tools[i].Lv), bonus, speed, yield)
|
||||||
|
}
|
||||||
|
}
|
||||||
var moonID string
|
var moonID string
|
||||||
if err := tx.QueryRow(ctx, "select source_id from resource_well where id = $1", wellID).Scan(&moonID); err != nil {
|
if err := tx.QueryRow(ctx, "select source_id from resource_well where id = $1", wellID).Scan(&moonID); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1690,7 +1566,7 @@ func sell(ctx context.Context, db *pgxpool.Pool, userID, resource string, amount
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
bonus, err := loadBonus(ctx, tx, userID, "")
|
bonus, err := loadBonus(ctx, tx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-75
@@ -4,7 +4,7 @@ package main
|
|||||||
// players are active: they claim (claim all with redeploy) as soon as a drill is ready (at
|
// players are active: they claim (claim all with redeploy) as soon as a drill is ready (at
|
||||||
// most every gsClaimGap), stake drills whose well ran dry, sell every half minute and buy, all within a
|
// most every gsClaimGap), stake drills whose well ran dry, sell every half minute and buy, all within a
|
||||||
// click budget (gsClicksPerSec). The "copies" player buys only drill copies (and moons); the
|
// click budget (gsClicksPerSec). The "copies" player buys only drill copies (and moons); the
|
||||||
// "boosts" player buys whatever adds the most ₥/minute per ₥ (upgrades, attachments,
|
// "boosts" player buys whatever adds the most ₥/minute per ₥ (a level on a yield or cycle track,
|
||||||
// advisors or copies). Both buy a moon when their drills are about to run dry. Every report
|
// advisors or copies). Both buy a moon when their drills are about to run dry. Every report
|
||||||
// row lists the best buy of each kind with what it adds per ₥ spent, in ₥/minute and in
|
// row lists the best buy of each kind with what it adds per ₥ spent, in ₥/minute and in
|
||||||
// ₥/click, and the summary compares the boosts with the copies.
|
// ₥/click, and the summary compares the boosts with the copies.
|
||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type gsPlayer struct {
|
type gsPlayer struct {
|
||||||
@@ -38,8 +37,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type gsTool struct {
|
type gsTool struct {
|
||||||
item, tier int
|
item int
|
||||||
mods []string
|
lv Levels
|
||||||
doneAt int // busy until then; -1 idle
|
doneAt int // busy until then; -1 idle
|
||||||
take []gsTake
|
take []gsTake
|
||||||
}
|
}
|
||||||
@@ -61,21 +60,20 @@ type gsMoon struct {
|
|||||||
free float64 // ore not yet taken or booked
|
free float64 // ore not yet taken or booked
|
||||||
}
|
}
|
||||||
|
|
||||||
// gsGroup is the drills of one kind (store item, tier, attachments); tool is one of them.
|
// gsGroup is the drills of one kind (store item, track levels); tool is one of them.
|
||||||
type gsGroup struct {
|
type gsGroup struct {
|
||||||
item, tier int
|
item int
|
||||||
mods []string
|
lv Levels
|
||||||
n, tool int
|
n, tool int
|
||||||
}
|
}
|
||||||
|
|
||||||
func gsKey(item, tier int, mods []string) string { return fmt.Sprint(item, tier, mods) }
|
|
||||||
|
|
||||||
// GSBuy is a candidate purchase: what it adds and what it costs.
|
// GSBuy is a candidate purchase: what it adds and what it costs.
|
||||||
type GSBuy struct {
|
type GSBuy struct {
|
||||||
Kind, Name string
|
Kind, Name string
|
||||||
Cost float64 // ₥
|
Cost float64 // ₥
|
||||||
Gain, Click float64 // ₥/hour added, ₥/click added
|
Gain, Click float64 // ₥/hour added, ₥/click added
|
||||||
tool int // upgrade or attachment: which tool; copy: which store item
|
tool int // upgrade: which tool; copy: which store item
|
||||||
|
track string // upgrade: yield or cycle
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b GSBuy) Payback() string {
|
func (b GSBuy) Payback() string {
|
||||||
@@ -162,7 +160,7 @@ func gradeStats(c *GameConfig) []GradeStat {
|
|||||||
if st.Drill != "" {
|
if st.Drill != "" {
|
||||||
st.Drill += ", "
|
st.Drill += ", "
|
||||||
}
|
}
|
||||||
perWell := item.ClaimAmount / float64(item.ToolReach(0))
|
perWell := item.ClaimAmount / float64(item.ToolReach())
|
||||||
st.Drill += fmt.Sprintf("%s (%.1f claims, %ds)", toolCode(item.Name), g.MeanSupply()/perWell,
|
st.Drill += fmt.Sprintf("%s (%.1f claims, %ds)", toolCode(item.Name), g.MeanSupply()/perWell,
|
||||||
item.CompletionTimeInSecs)
|
item.CompletionTimeInSecs)
|
||||||
}
|
}
|
||||||
@@ -180,17 +178,17 @@ func gradeStats(c *GameConfig) []GradeStat {
|
|||||||
func gsEval(c *GameConfig, stats []GradeStat, caps func(int) ReachCaps, grade int, gs []gsGroup, adv []string) (inc, clk float64) {
|
func gsEval(c *GameConfig, stats []GradeStat, caps func(int) ReachCaps, grade int, gs []gsGroup, adv []string) (inc, clk float64) {
|
||||||
st := stats[grade]
|
st := stats[grade]
|
||||||
stakes := 0.0
|
stakes := 0.0
|
||||||
cycles := map[int]bool{}
|
cycles := map[float64]bool{}
|
||||||
|
b := c.bonusesFor(adv)
|
||||||
for _, x := range gs {
|
for _, x := range gs {
|
||||||
if x.n == 0 {
|
if x.n == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
it := &c.StoreItems[x.item]
|
it := &c.StoreItems[x.item]
|
||||||
b := c.bonusesFor(adv, x.mods)
|
secs, y := drillStats(it.ToolSecs(x.lv), it.ToolYield(x.lv), b, 1, 1)
|
||||||
secs, y := drillStats(it.ToolSecs(x.tier), it.ToolYield(x.tier), b, 1, 1)
|
reach := it.ToolReach()
|
||||||
reach := it.ToolReach(x.tier)
|
|
||||||
cycles[max(secs, gsClaimGap)] = true
|
cycles[max(secs, gsClaimGap)] = true
|
||||||
p := float64(max(secs, gsClaimGap)) + 0.5 // waits half a tick on average
|
p := max(secs, gsClaimGap) + 0.5 // waits half a tick on average
|
||||||
n := float64(x.n)
|
n := float64(x.n)
|
||||||
inc += n * caps(grade).Yield(y, reach) * st.Unit * b.Sell * 3600 / p
|
inc += n * caps(grade).Yield(y, reach) * st.Unit * b.Sell * 3600 / p
|
||||||
per := st.Units / max(1, st.Wells) / y
|
per := st.Units / max(1, st.Wells) / y
|
||||||
@@ -201,7 +199,7 @@ func gsEval(c *GameConfig, stats []GradeStat, caps func(int) ReachCaps, grade in
|
|||||||
}
|
}
|
||||||
claims := 0.0
|
claims := 0.0
|
||||||
for secs := range cycles {
|
for secs := range cycles {
|
||||||
claims += 3600 / float64(secs)
|
claims += 3600 / secs
|
||||||
}
|
}
|
||||||
clk = min(claims, 3600/gsClaimGap) + stakes + 3600/gsSellEvery*min(5, math.Round(st.Wells))
|
clk = min(claims, 3600/gsClaimGap) + stakes + 3600/gsSellEvery*min(5, math.Round(st.Wells))
|
||||||
if budget := 3600 * gsClicksPerSec; clk > budget {
|
if budget := 3600 * gsClicksPerSec; clk > budget {
|
||||||
@@ -250,30 +248,29 @@ func simulatePlayer(c *GameConfig, pl gsPlayer, stats []GradeStat) *GSResult {
|
|||||||
idx := map[string]int{}
|
idx := map[string]int{}
|
||||||
var out []gsGroup
|
var out []gsGroup
|
||||||
for j, t := range tools {
|
for j, t := range tools {
|
||||||
k := gsKey(t.item, t.tier, t.mods)
|
k := fmt.Sprint(t.item, t.lv)
|
||||||
if i, ok := idx[k]; ok {
|
if i, ok := idx[k]; ok {
|
||||||
out[i].n++
|
out[i].n++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
idx[k] = len(out)
|
idx[k] = len(out)
|
||||||
out = append(out, gsGroup{t.item, t.tier, t.mods, 1, j})
|
out = append(out, gsGroup{t.item, t.lv, 1, j})
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
// with is gs with one drill of group k (none if k < 0) turned into (item, tier, mods).
|
// with is gs with one drill of group k (none if k < 0) turned into (item, lv).
|
||||||
with := func(gs []gsGroup, k, item, tier int, mods []string) []gsGroup {
|
with := func(gs []gsGroup, k, item int, lv Levels) []gsGroup {
|
||||||
out := slices.Clone(gs)
|
out := slices.Clone(gs)
|
||||||
if k >= 0 {
|
if k >= 0 {
|
||||||
out[k].n--
|
out[k].n--
|
||||||
}
|
}
|
||||||
key := gsKey(item, tier, mods)
|
|
||||||
for i := range out {
|
for i := range out {
|
||||||
if gsKey(out[i].item, out[i].tier, out[i].mods) == key {
|
if out[i].item == item && out[i].lv == lv {
|
||||||
out[i].n++
|
out[i].n++
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return append(out, gsGroup{item, tier, mods, 1, -1})
|
return append(out, gsGroup{item, lv, 1, -1})
|
||||||
}
|
}
|
||||||
eval := func(gs []gsGroup, adv []string) (float64, float64) { return gsEval(c, stats, caps, grade, gs, adv) }
|
eval := func(gs []gsGroup, adv []string) (float64, float64) { return gsEval(c, stats, caps, grade, gs, adv) }
|
||||||
partsCost := func(parts map[string]float64) (float64, bool) {
|
partsCost := func(parts map[string]float64) (float64, bool) {
|
||||||
@@ -294,26 +291,21 @@ func simulatePlayer(c *GameConfig, pl gsPlayer, stats []GradeStat) *GSResult {
|
|||||||
var out []GSBuy
|
var out []GSBuy
|
||||||
if len(tools) < gsMaxTools {
|
if len(tools) < gsMaxTools {
|
||||||
for i, it := range c.StoreItems {
|
for i, it := range c.StoreItems {
|
||||||
out = append(out, buy("copy", toolCode(it.Name), it.Price, i, with(gs, -1, i, 0, nil), advisors))
|
out = append(out, buy("copy", toolCode(it.Name), it.Price, i, with(gs, -1, i, Levels{}), advisors))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for k, x := range gs {
|
for k, x := range gs {
|
||||||
it := &c.StoreItems[x.item]
|
it := &c.StoreItems[x.item]
|
||||||
if u := it.NextUpgrade(x.tier); u != nil {
|
for _, kind := range trackKinds {
|
||||||
|
n := x.lv.Of(kind) + 1
|
||||||
|
if u := it.TrackLevel(kind, n); u != nil {
|
||||||
if pc, ok := partsCost(u.Parts); ok {
|
if pc, ok := partsCost(u.Parts); ok {
|
||||||
out = append(out, buy("upgrade", fmt.Sprintf("%s t%d", toolCode(it.Name), u.Tier), u.Price+pc,
|
b := buy("upgrade", fmt.Sprintf("%s %s%d", toolCode(it.Name), kind[:1], n), u.Price+pc,
|
||||||
x.tool, with(gs, k, x.item, u.Tier, x.mods), advisors))
|
x.tool, with(gs, k, x.item, x.lv.With(kind, n)), advisors)
|
||||||
|
b.track = kind
|
||||||
|
out = append(out, b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(x.mods) < c.Attachments.SlotsPerTool {
|
|
||||||
for _, m := range c.Attachments.Items {
|
|
||||||
if slices.Contains(x.mods, m.Name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mods := append(slices.Clone(x.mods), m.Name)
|
|
||||||
out = append(out, buy("attachment", m.Name+" on "+toolCode(it.Name), m.Price, x.tool,
|
|
||||||
with(gs, k, x.item, x.tier, mods), advisors))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(advisors) < seats || seats-1 < len(c.Advisors.SeatPrices) {
|
if len(advisors) < seats || seats-1 < len(c.Advisors.SeatPrices) {
|
||||||
@@ -347,7 +339,7 @@ func simulatePlayer(c *GameConfig, pl gsPlayer, stats []GradeStat) *GSResult {
|
|||||||
sum := 0.0
|
sum := 0.0
|
||||||
for _, t := range tools {
|
for _, t := range tools {
|
||||||
it := &c.StoreItems[t.item]
|
it := &c.StoreItems[t.item]
|
||||||
_, y := drillStats(it.ToolSecs(t.tier), it.ToolYield(t.tier), c.bonusesFor(advisors, t.mods), 1, 1)
|
_, y := drillStats(it.ToolSecs(t.lv), it.ToolYield(t.lv), c.bonusesFor(advisors), 1, 1)
|
||||||
sum += 2 * y
|
sum += 2 * y
|
||||||
}
|
}
|
||||||
return sum
|
return sum
|
||||||
@@ -394,7 +386,7 @@ func simulatePlayer(c *GameConfig, pl gsPlayer, stats []GradeStat) *GSResult {
|
|||||||
// book stakes a tool on moon mi: on well only (a one-well redeploy), else its fullest wells.
|
// book stakes a tool on moon mi: on well only (a one-well redeploy), else its fullest wells.
|
||||||
book := func(t int, tl *gsTool, mi, only int) bool {
|
book := func(t int, tl *gsTool, mi, only int) bool {
|
||||||
it := &c.StoreItems[tl.item]
|
it := &c.StoreItems[tl.item]
|
||||||
secs, y := drillStats(it.ToolSecs(tl.tier), it.ToolYield(tl.tier), c.bonusesFor(advisors, tl.mods), 1, 1)
|
secs, y := drillStats(it.ToolSecs(tl.lv), it.ToolYield(tl.lv), c.bonusesFor(advisors), 1, 1)
|
||||||
m := moons[mi]
|
m := moons[mi]
|
||||||
var order []int
|
var order []int
|
||||||
if only >= 0 {
|
if only >= 0 {
|
||||||
@@ -406,9 +398,9 @@ func simulatePlayer(c *GameConfig, pl gsPlayer, stats []GradeStat) *GSResult {
|
|||||||
slices.SortFunc(order, func(a, b int) int {
|
slices.SortFunc(order, func(a, b int) int {
|
||||||
return cmpDesc(m.wells[a].left-m.wells[a].booked, m.wells[b].left-m.wells[b].booked)
|
return cmpDesc(m.wells[a].left-m.wells[a].booked, m.wells[b].left-m.wells[b].booked)
|
||||||
})
|
})
|
||||||
order = order[:min(it.ToolReach(tl.tier), len(order))]
|
order = order[:min(it.ToolReach(), len(order))]
|
||||||
}
|
}
|
||||||
share := y / float64(it.ToolReach(tl.tier))
|
share := y / float64(it.ToolReach())
|
||||||
tl.take = tl.take[:0]
|
tl.take = tl.take[:0]
|
||||||
for _, wi := range order {
|
for _, wi := range order {
|
||||||
w := &m.wells[wi]
|
w := &m.wells[wi]
|
||||||
@@ -423,7 +415,7 @@ func simulatePlayer(c *GameConfig, pl gsPlayer, stats []GradeStat) *GSResult {
|
|||||||
if len(tl.take) == 0 {
|
if len(tl.take) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
tl.doneAt = t + secs
|
tl.doneAt = t + int(math.Ceil(secs-1e-9))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// deploy stakes an idle tool on the moon with the most free ore.
|
// deploy stakes an idle tool on the moon with the most free ore.
|
||||||
@@ -471,7 +463,7 @@ func simulatePlayer(c *GameConfig, pl gsPlayer, stats []GradeStat) *GSResult {
|
|||||||
// Redeploy: the same well for a one-well drill, the same moon for the others.
|
// Redeploy: the same well for a one-well drill, the same moon for the others.
|
||||||
if len(prev) > 0 {
|
if len(prev) > 0 {
|
||||||
only := -1
|
only := -1
|
||||||
if c.StoreItems[tl.item].ToolReach(tl.tier) == 1 {
|
if c.StoreItems[tl.item].ToolReach() == 1 {
|
||||||
only = prev[0].well
|
only = prev[0].well
|
||||||
}
|
}
|
||||||
book(t, tl, prev[0].moon, only)
|
book(t, tl, prev[0].moon, only)
|
||||||
@@ -489,7 +481,7 @@ func simulatePlayer(c *GameConfig, pl gsPlayer, stats []GradeStat) *GSResult {
|
|||||||
pressure[name] = c.MarketPressure.Decay(pressure[name], 1)
|
pressure[name] = c.MarketPressure.Decay(pressure[name], 1)
|
||||||
}
|
}
|
||||||
if t-lastSell >= gsSellEvery {
|
if t-lastSell >= gsSellEvery {
|
||||||
sell := c.bonusesFor(advisors, nil).Sell
|
sell := c.bonusesFor(advisors).Sell
|
||||||
tick := int64(t / 300)
|
tick := int64(t / 300)
|
||||||
left := false
|
left := false
|
||||||
for _, name := range resources {
|
for _, name := range resources {
|
||||||
@@ -556,7 +548,7 @@ func simulatePlayer(c *GameConfig, pl gsPlayer, stats []GradeStat) *GSResult {
|
|||||||
row := GSRow{At: SimTime(t), Credits: credits, Tools: len(tools), Grade: c.MoonGrades[grade].Grade,
|
row := GSRow{At: SimTime(t), Credits: credits, Tools: len(tools), Grade: c.MoonGrades[grade].Grade,
|
||||||
Income: (earned - lastEarned) * 3600 / dt, PerClick: (earned - lastEarned) / float64(max(1, clicks-lastClicks)),
|
Income: (earned - lastEarned) * 3600 / dt, PerClick: (earned - lastEarned) / float64(max(1, clicks-lastClicks)),
|
||||||
ClickMin: float64(clicks-lastClicks) * 60 / dt, Best: map[string]GSBuy{}, BestKind: best("", pl.All).Kind}
|
ClickMin: float64(clicks-lastClicks) * 60 / dt, Best: map[string]GSBuy{}, BestKind: best("", pl.All).Kind}
|
||||||
for _, k := range []string{"copy", "upgrade", "attachment", "advisor"} {
|
for _, k := range []string{"copy", "upgrade", "advisor"} {
|
||||||
row.Best[k] = best(k, true)
|
row.Best[k] = best(k, true)
|
||||||
}
|
}
|
||||||
res.Rows = append(res.Rows, row)
|
res.Rows = append(res.Rows, row)
|
||||||
@@ -575,8 +567,8 @@ func simulatePlayer(c *GameConfig, pl gsPlayer, stats []GradeStat) *GSResult {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
cost := 1.0
|
cost := 1.0
|
||||||
if b.Kind == "attachment" || (b.Kind == "advisor" && len(advisors) >= seats) {
|
if b.Kind == "advisor" && len(advisors) >= seats {
|
||||||
cost = 2 // buy and equip, buy a seat and hire
|
cost = 2 // buy a seat and hire
|
||||||
}
|
}
|
||||||
if bank < cost {
|
if bank < cost {
|
||||||
wantBuy = true
|
wantBuy = true
|
||||||
@@ -594,10 +586,8 @@ func simulatePlayer(c *GameConfig, pl gsPlayer, stats []GradeStat) *GSResult {
|
|||||||
case "copy":
|
case "copy":
|
||||||
tools = append(tools, gsTool{item: b.tool, doneAt: -1})
|
tools = append(tools, gsTool{item: b.tool, doneAt: -1})
|
||||||
case "upgrade":
|
case "upgrade":
|
||||||
tools[b.tool].tier++
|
lv := tools[b.tool].lv
|
||||||
case "attachment":
|
tools[b.tool].lv = lv.With(b.track, lv.Of(b.track)+1)
|
||||||
name := b.Name[:strings.Index(b.Name, " on ")]
|
|
||||||
tools[b.tool].mods = append(slices.Clone(tools[b.tool].mods), name)
|
|
||||||
case "advisor":
|
case "advisor":
|
||||||
if len(advisors) >= seats {
|
if len(advisors) >= seats {
|
||||||
seats++
|
seats++
|
||||||
@@ -626,8 +616,8 @@ func printGradeSim(c *GameConfig) {
|
|||||||
for _, pl := range gsPlayers {
|
for _, pl := range gsPlayers {
|
||||||
r := simulatePlayer(c, pl, stats)
|
r := simulatePlayer(c, pl, stats)
|
||||||
fmt.Printf("\n%s player\n", pl.Name)
|
fmt.Printf("\n%s player\n", pl.Name)
|
||||||
fmt.Printf("%7s %9s %9s %6s %5s %5s %3s %-26s %-26s %-34s %-28s %s\n", "at", "credits", "₥/h", "₥/clk", "clk/m", "tools", "gr",
|
fmt.Printf("%7s %9s %9s %6s %5s %5s %3s %-26s %-28s %-28s %s\n", "at", "credits", "₥/h", "₥/clk", "clk/m", "tools", "gr",
|
||||||
"best copy (/min, /clk)", "best upgrade", "best attachment", "best advisor", "buys")
|
"best copy (/min, /clk)", "best upgrade", "best advisor", "buys")
|
||||||
cell := func(b GSBuy) string {
|
cell := func(b GSBuy) string {
|
||||||
if b.Kind == "" {
|
if b.Kind == "" {
|
||||||
return "-"
|
return "-"
|
||||||
@@ -635,9 +625,9 @@ func printGradeSim(c *GameConfig) {
|
|||||||
return fmt.Sprintf("%s %.3g %.3g", b.Name, b.PerMin(), b.PerClick())
|
return fmt.Sprintf("%s %.3g %.3g", b.Name, b.PerMin(), b.PerClick())
|
||||||
}
|
}
|
||||||
for _, row := range r.Rows {
|
for _, row := range r.Rows {
|
||||||
fmt.Printf("%7s %9.0f %9.0f %6.0f %5.1f %5d %3s %-26s %-26s %-34s %-28s %s\n", row.At, row.Credits, row.Income,
|
fmt.Printf("%7s %9.0f %9.0f %6.0f %5.1f %5d %3s %-26s %-28s %-28s %s\n", row.At, row.Credits, row.Income,
|
||||||
row.PerClick, row.ClickMin, row.Tools, row.Grade, cell(row.Best["copy"]), cell(row.Best["upgrade"]),
|
row.PerClick, row.ClickMin, row.Tools, row.Grade, cell(row.Best["copy"]), cell(row.Best["upgrade"]),
|
||||||
cell(row.Best["attachment"]), cell(row.Best["advisor"]), row.BestKind)
|
cell(row.Best["advisor"]), row.BestKind)
|
||||||
}
|
}
|
||||||
fmt.Print("first moon of each grade (clicks by then):")
|
fmt.Print("first moon of each grade (clicks by then):")
|
||||||
for _, g := range c.MoonGrades {
|
for _, g := range c.MoonGrades {
|
||||||
@@ -654,7 +644,7 @@ func printGradeSim(c *GameConfig) {
|
|||||||
fmt.Printf("\nlongest stretch with nothing to click: %s (from %s); with nothing to buy: %s (from %s), before the top grade %s\nbought: %v\n",
|
fmt.Printf("\nlongest stretch with nothing to click: %s (from %s); with nothing to buy: %s (from %s), before the top grade %s\nbought: %v\n",
|
||||||
r.Idle, r.IdleAt, r.BuyGap, r.BuyGapAt, r.BuyGapPreS, r.Bought)
|
r.Idle, r.IdleAt, r.BuyGap, r.BuyGapAt, r.BuyGapPreS, r.Bought)
|
||||||
fmt.Print("purchases (count, ₥ spent, ₥/min and ₥/click added per 1k₥):")
|
fmt.Print("purchases (count, ₥ spent, ₥/min and ₥/click added per 1k₥):")
|
||||||
for _, k := range []string{"copy", "upgrade", "attachment", "advisor"} {
|
for _, k := range []string{"copy", "upgrade", "advisor"} {
|
||||||
if sp := r.Spent[k]; sp > 0 {
|
if sp := r.Spent[k]; sp > 0 {
|
||||||
fmt.Printf(" %s %d %s %.3g %.3g;", k, r.Bought[k], formatCompact(sp), r.GainMin[k]/sp*1000, r.GainClick[k]/sp*1000)
|
fmt.Printf(" %s %d %s %.3g %.3g;", k, r.Bought[k], formatCompact(sp), r.GainMin[k]/sp*1000, r.GainClick[k]/sp*1000)
|
||||||
}
|
}
|
||||||
@@ -664,8 +654,8 @@ func printGradeSim(c *GameConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// printBoostTable compares, for each drill alone on the grade it's meant for, a second copy
|
// printBoostTable compares, for each drill alone on the grade it's meant for, a second copy
|
||||||
// with its boosts: what each adds per 1k₥, in ₥/minute and ₥/click; boosts as a ratio to
|
// with its upgrades: what each adds per 1k₥, in ₥/minute and ₥/click; upgrades as a ratio
|
||||||
// the copy.
|
// to the copy: each track's first level, the whole track, and both tracks to the top.
|
||||||
func printBoostTable(c *GameConfig, stats []GradeStat) {
|
func printBoostTable(c *GameConfig, stats []GradeStat) {
|
||||||
capsBy := map[int]ReachCaps{}
|
capsBy := map[int]ReachCaps{}
|
||||||
caps := func(g int) ReachCaps {
|
caps := func(g int) ReachCaps {
|
||||||
@@ -674,33 +664,41 @@ func printBoostTable(c *GameConfig, stats []GradeStat) {
|
|||||||
}
|
}
|
||||||
return capsBy[g]
|
return capsBy[g]
|
||||||
}
|
}
|
||||||
fmt.Printf("\na drill alone on its grade: a copy (₥/min, ₥/click per 1k₥), boosts ÷ copy (₥/min, ₥/click)\n%-9s %2s %13s %13s", "drill", "gr", "copy", "upgrades t1-5")
|
fmt.Printf("\na drill alone on its grade: a copy (₥/min, ₥/click per 1k₥), upgrades ÷ copy (₥/min, ₥/click)\n%-9s %2s %13s %15s %15s %15s %15s %15s\n",
|
||||||
for _, m := range c.Attachments.Items {
|
"drill", "gr", "copy", "yield 1", "yield all", "cycle 1", "cycle all", "both all")
|
||||||
fmt.Printf(" %13s", m.Name)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
for i := range c.StoreItems {
|
for i := range c.StoreItems {
|
||||||
it := &c.StoreItems[i]
|
it := &c.StoreItems[i]
|
||||||
g := c.gradeIndex(suitedGrade(c, it).Grade)
|
g := c.gradeIndex(suitedGrade(c, it).Grade)
|
||||||
base := []gsGroup{{i, 0, nil, 1, 0}}
|
inc0, clk0 := gsEval(c, stats, caps, g, []gsGroup{{i, Levels{}, 1, 0}}, nil)
|
||||||
inc0, clk0 := gsEval(c, stats, caps, g, base, nil)
|
|
||||||
per := func(gs []gsGroup, cost float64) (float64, float64) {
|
per := func(gs []gsGroup, cost float64) (float64, float64) {
|
||||||
inc, clk := gsEval(c, stats, caps, g, gs, nil)
|
inc, clk := gsEval(c, stats, caps, g, gs, nil)
|
||||||
return (inc - inc0) / 60 / cost * 1000, (inc/clk - inc0/clk0) / cost * 1000
|
return (inc - inc0) / 60 / cost * 1000, (inc/clk - inc0/clk0) / cost * 1000
|
||||||
}
|
}
|
||||||
cm, cc := per([]gsGroup{{i, 0, nil, 2, 0}}, it.Price)
|
cm, cc := per([]gsGroup{{i, Levels{}, 2, 0}}, it.Price)
|
||||||
fmt.Printf("%-9s %2s %6.3g %6.3g", toolCode(it.Name), c.MoonGrades[g].Grade, cm, cc)
|
fmt.Printf("%-9s %2s %6.3g %6.3g", toolCode(it.Name), c.MoonGrades[g].Grade, cm, cc)
|
||||||
cost, top := 0.0, 0
|
cost := func(kind string, upTo int) float64 {
|
||||||
for _, u := range it.Upgrades {
|
sum := 0.0
|
||||||
cost += u.Price + partsValue(c, u.Parts)
|
for n := 1; n <= upTo; n++ {
|
||||||
top = max(top, u.Tier)
|
if l := it.TrackLevel(kind, n); l != nil {
|
||||||
|
sum += l.Price + partsValue(c, l.Parts)
|
||||||
}
|
}
|
||||||
if top > 0 {
|
|
||||||
m, k := per([]gsGroup{{i, top, nil, 1, 0}}, cost)
|
|
||||||
fmt.Printf(" ×%5.2f ×%5.2f", m/cm, k/cc)
|
|
||||||
}
|
}
|
||||||
for _, a := range c.Attachments.Items {
|
return sum
|
||||||
m, k := per([]gsGroup{{i, 0, []string{a.Name}, 1, 0}}, a.Price)
|
}
|
||||||
|
top := it.MaxLevels()
|
||||||
|
for _, v := range []struct {
|
||||||
|
lv Levels
|
||||||
|
cost float64
|
||||||
|
}{
|
||||||
|
{Levels{Yield: 1}, cost("yield", 1)}, {Levels{Yield: top.Yield}, cost("yield", top.Yield)},
|
||||||
|
{Levels{Cycle: 1}, cost("cycle", 1)}, {Levels{Cycle: top.Cycle}, cost("cycle", top.Cycle)},
|
||||||
|
{top, cost("yield", top.Yield) + cost("cycle", top.Cycle)},
|
||||||
|
} {
|
||||||
|
if v.cost <= 0 {
|
||||||
|
fmt.Printf(" %13s", "-")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m, k := per([]gsGroup{{i, v.lv, 1, 0}}, v.cost)
|
||||||
fmt.Printf(" ×%5.2f ×%5.2f", m/cm, k/cc)
|
fmt.Printf(" ×%5.2f ×%5.2f", m/cm, k/cc)
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func logEvent(ctx context.Context, q querier, userID, kind string, data map[stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Credits spent are logged as "price" on these kinds, earned as "credits" on sell.
|
// Credits spent are logged as "price" on these kinds, earned as "credits" on sell.
|
||||||
|
// buy_attachment: attachments were sold until migration 020 (kept for older events).
|
||||||
const spendKinds = "'buy_moon', 'buy_tool', 'upgrade', 'buy_slot', 'buy_attachment', 'hire_advisor', 'buy_seat'"
|
const spendKinds = "'buy_moon', 'buy_tool', 'upgrade', 'buy_slot', 'buy_attachment', 'hire_advisor', 'buy_seat'"
|
||||||
|
|
||||||
// ------------------------------------------------------------------ dashboard
|
// ------------------------------------------------------------------ dashboard
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ package main
|
|||||||
//
|
//
|
||||||
// users.onboarding holds flags, which are only ever added:
|
// users.onboarding holds flags, which are only ever added:
|
||||||
// - 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 or
|
// refinery (bought a moon of refinery.unlockGrade or higher), advisors (bought a tool);
|
||||||
// attachment);
|
|
||||||
// - claimed:<resource>, the resources claimed so far;
|
// - claimed:<resource>, the resources claimed so far;
|
||||||
// - 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",
|
||||||
// Help, Leaderboard, moon sale, "Claim and redeploy", upgrades, attachments and selling
|
// Help, Leaderboard, moon sale, "Claim and redeploy", upgrades (tracks.go) and selling
|
||||||
// tools (the drill panel opens with Equipment and shows these parts once the Store is
|
// tools (the drill panel opens with Equipment and shows these parts once the Store is
|
||||||
// unlocked); Claim all also needs 2 tools.
|
// unlocked); Claim all also needs 2 tools.
|
||||||
|
|
||||||
@@ -126,9 +125,8 @@ func markSeen(ctx context.Context, db *pgxpool.Pool, userID, section string) err
|
|||||||
|
|
||||||
// actionGates is the feature each action needs; runAction refuses the rest.
|
// actionGates is the feature each action needs; runAction refuses the rest.
|
||||||
var actionGates = map[string]string{
|
var actionGates = map[string]string{
|
||||||
"sell-moon": "store", "sell-tool": "store", "sell-attachment": "store",
|
"sell-moon": "store", "sell-tool": "store",
|
||||||
"buy-tool": "store", "upgrade": "store", "buy-attachment": "store",
|
"buy-tool": "store", "upgrade": "store", "redrill": "market", "new-game": "help",
|
||||||
"equip": "store", "unequip": "store", "redrill": "market", "new-game": "help",
|
|
||||||
"sell": "market",
|
"sell": "market",
|
||||||
"refine": "refinery", "collect": "refinery", "buy-slot": "refinery", "upgrade-batch": "refinery",
|
"refine": "refinery", "collect": "refinery", "buy-slot": "refinery", "upgrade-batch": "refinery",
|
||||||
"hire": "advisors", "buy-seat": "advisors", "seat-advisor": "advisors", "unseat-advisor": "advisors",
|
"hire": "advisors", "buy-seat": "advisors", "seat-advisor": "advisors", "unseat-advisor": "advisors",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ var odT0 = time.Unix(1_000_000, 0)
|
|||||||
|
|
||||||
func odStake(n int, well string, amount float64, reach int) Stake {
|
func odStake(n int, well string, amount float64, reach int) Stake {
|
||||||
return Stake{ID: fmt.Sprintf("s%d", n), MoonID: "m", WellID: well, Amount: amount, Reach: reach,
|
return Stake{ID: fmt.Sprintf("s%d", n), MoonID: "m", WellID: well, Amount: amount, Reach: reach,
|
||||||
Start: odT0.Add(time.Duration(n) * time.Second), DurationInSecs: 10 + n}
|
Start: odT0.Add(time.Duration(n) * time.Second), DurationInSecs: float64(10 + n)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func near(a, b float64) bool { return math.Abs(a-b) < 1e-9 }
|
func near(a, b float64) bool { return math.Abs(a-b) < 1e-9 }
|
||||||
|
|||||||
+14
-12
@@ -202,7 +202,7 @@ func (s *server) forbidden(w http.ResponseWriter, name string, data any) {
|
|||||||
type PlayerTool struct {
|
type PlayerTool struct {
|
||||||
Tool
|
Tool
|
||||||
Item *StoreItem
|
Item *StoreItem
|
||||||
MaxTier int
|
Max Levels // the top level of each track
|
||||||
Stake *Stake
|
Stake *Stake
|
||||||
MoonName string
|
MoonName string
|
||||||
Remaining time.Duration
|
Remaining time.Duration
|
||||||
@@ -291,7 +291,7 @@ func (s *server) loadPlayerPage(ctx context.Context, userID, adminID string) (*P
|
|||||||
for _, t := range st.Tools {
|
for _, t := range st.Tools {
|
||||||
pt := PlayerTool{Tool: t, Item: st.StoreItem(t.StoreItemID), Stake: st.StakeForTool(t.ID)}
|
pt := PlayerTool{Tool: t, Item: st.StoreItem(t.StoreItemID), Stake: st.StakeForTool(t.ID)}
|
||||||
if pt.Item != nil {
|
if pt.Item != nil {
|
||||||
pt.MaxTier = len(pt.Item.Upgrades)
|
pt.Max = pt.Item.MaxLevels()
|
||||||
}
|
}
|
||||||
if pt.Stake != nil {
|
if pt.Stake != nil {
|
||||||
pt.MoonName = st.MoonName(pt.Stake.MoonID)
|
pt.MoonName = st.MoonName(pt.Stake.MoonID)
|
||||||
@@ -402,10 +402,12 @@ func (s *server) handlePlayerAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
case "tool":
|
case "tool":
|
||||||
item, _ := strconv.Atoi(r.FormValue("item"))
|
item, _ := strconv.Atoi(r.FormValue("item"))
|
||||||
tier, _ := strconv.Atoi(r.FormValue("tier"))
|
var lv Levels
|
||||||
|
lv.Yield, _ = strconv.Atoi(r.FormValue("yield"))
|
||||||
|
lv.Cycle, _ = strconv.Atoi(r.FormValue("cycle"))
|
||||||
var name string
|
var name string
|
||||||
if name, err = adminTool(ctx, s.db, adminID, id, item, tier); err == nil {
|
if name, err = adminTool(ctx, s.db, adminID, id, item, lv); err == nil {
|
||||||
message = fmt.Sprintf("Granted %s (tier %d)", name, tier)
|
message = fmt.Sprintf("Granted %s (yield level %d, cycle level %d)", name, lv.Yield, lv.Cycle)
|
||||||
}
|
}
|
||||||
case "finish-stakes":
|
case "finish-stakes":
|
||||||
var n int
|
var n int
|
||||||
@@ -550,8 +552,8 @@ func adminResources(ctx context.Context, db *pgxpool.Pool, adminID, userID, name
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// adminTool gives the player a store item at a tier (0 is the base tool).
|
// adminTool gives the player a store item at track levels (0 is the base tool).
|
||||||
func adminTool(ctx context.Context, db *pgxpool.Pool, adminID, userID string, itemID, tier int) (string, error) {
|
func adminTool(ctx context.Context, db *pgxpool.Pool, adminID, userID string, itemID int, lv Levels) (string, error) {
|
||||||
var item *StoreItem
|
var item *StoreItem
|
||||||
items := cfg().StoreItems
|
items := cfg().StoreItems
|
||||||
for i := range items {
|
for i := range items {
|
||||||
@@ -562,21 +564,21 @@ func adminTool(ctx context.Context, db *pgxpool.Pool, adminID, userID string, it
|
|||||||
if item == nil {
|
if item == nil {
|
||||||
return "", gameErr("Pick a tool")
|
return "", gameErr("Pick a tool")
|
||||||
}
|
}
|
||||||
if tier < 0 || tier > len(item.Upgrades) {
|
if top := item.MaxLevels(); lv.Yield < 0 || lv.Cycle < 0 || lv.Yield > top.Yield || lv.Cycle > top.Cycle {
|
||||||
return "", gameErr("%s has tiers 0 to %d", item.Name, len(item.Upgrades))
|
return "", gameErr("%s has yield levels 0 to %d and cycle levels 0 to %d", item.Name, top.Yield, top.Cycle)
|
||||||
}
|
}
|
||||||
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
||||||
if _, err := lockPlayer(ctx, tx, userID); err != nil {
|
if _, err := lockPlayer(ctx, tx, userID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := tx.Exec(ctx,
|
_, err := tx.Exec(ctx,
|
||||||
"insert into inventory_item(user_id, store_item_id, tier) values ($1, $2, $3)",
|
"insert into inventory_item(user_id, store_item_id, yield_level, cycle_level) values ($1, $2, $3, $4)",
|
||||||
userID, itemID, tier)
|
userID, itemID, lv.Yield, lv.Cycle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return logEvent(ctx, tx, userID, "admin_grant",
|
return logEvent(ctx, tx, userID, "admin_grant",
|
||||||
map[string]any{"admin": adminID, "item": itemID, "name": item.Name, "tier": tier})
|
map[string]any{"admin": adminID, "item": itemID, "name": item.Name, "yieldLevel": lv.Yield, "cycleLevel": lv.Cycle})
|
||||||
})
|
})
|
||||||
return item.Name, err
|
return item.Name, err
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-26
@@ -2,9 +2,9 @@ package main
|
|||||||
|
|
||||||
// The refinery turns raw resources into refined items on a timer. A batch (recipe ×
|
// The refinery turns raw resources into refined items on a timer. A batch (recipe ×
|
||||||
// quantity) takes its inputs when it starts and fills one slot until the player collects
|
// quantity) takes its inputs when it starts and fills one slot until the player collects
|
||||||
// it. Refined items sell in the market and are parts for higher tool upgrades.
|
// it. Refined items sell in the market and are parts for the higher levels of the drills'
|
||||||
// Recipes, prices, slot prices and upgrade parts come from config/game.json ("refinery",
|
// upgrade tracks (tracks.go). Recipes, prices, slot prices and upgrade parts come from
|
||||||
// and "parts" on upgrade tiers).
|
// config/game.json ("refinery", and "parts" on track levels).
|
||||||
//
|
//
|
||||||
// Lock order, shared with game.go: users row (refinery actions) → bank_account →
|
// Lock order, shared with game.go: users row (refinery actions) → bank_account →
|
||||||
// resource_account → refined_account.
|
// resource_account → refined_account.
|
||||||
@@ -168,27 +168,15 @@ func (cfg *GameConfig) validateRefinery() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, si := range cfg.StoreItems {
|
for _, si := range cfg.StoreItems {
|
||||||
for _, u := range si.Upgrades {
|
for _, kind := range trackKinds {
|
||||||
for name, qty := range u.Parts {
|
for i, l := range si.Track(kind).Levels {
|
||||||
|
for name, qty := range l.Parts {
|
||||||
if _, ok := tiers[name]; !ok || !(qty > 0) {
|
if _, ok := tiers[name]; !ok || !(qty > 0) {
|
||||||
return fmt.Errorf("store item %d tier %d: part %q must be a refined item with a positive amount", si.ID, u.Tier, name)
|
return fmt.Errorf("store item %d %s level %d: part %q must be a refined item with a positive amount", si.ID, kind, i+1, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// upgradeParts is what upgrading a tool to the given tier uses up, from the config.
|
|
||||||
func upgradeParts(storeItemID, tier int) map[string]float64 {
|
|
||||||
for _, item := range cfg().StoreItems {
|
|
||||||
if item.ID == storeItemID {
|
|
||||||
for _, u := range item.Upgrades {
|
|
||||||
if u.Tier == tier {
|
|
||||||
return u.Parts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -427,7 +415,7 @@ func startRefine(ctx context.Context, db *pgxpool.Pool, userID, name string, qty
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Advisors with a refinery bonus too (bonus.go).
|
// Advisors with a refinery bonus too (bonus.go).
|
||||||
bonus, err := loadBonus(ctx, tx, userID, "")
|
bonus, err := loadBonus(ctx, tx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -583,7 +571,7 @@ func sellRefined(ctx context.Context, db *pgxpool.Pool, userID, name string, amo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
bonus, err := loadBonus(ctx, tx, userID, "")
|
bonus, err := loadBonus(ctx, tx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -687,11 +675,6 @@ func (s *State) partRows(need map[string]float64) []PartRow {
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
// upgradeParts is the parts an upgrade to the given tier needs, with what the player has.
|
|
||||||
func (s *State) upgradeParts(storeItemID, tier int) []PartRow {
|
|
||||||
return s.partRows(upgradeParts(storeItemID, tier))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) refineryPage() *RefineryPage {
|
func (s *State) refineryPage() *RefineryPage {
|
||||||
c := cfg()
|
c := cfg()
|
||||||
maxBatch := c.Refinery.maxBatchAt(s.RefineryBatchLevel)
|
maxBatch := c.Refinery.maxBatchAt(s.RefineryBatchLevel)
|
||||||
|
|||||||
+15
-54
@@ -293,8 +293,8 @@ func sellMoon(ctx context.Context, db *pgxpool.Pool, userID, moonID string) (str
|
|||||||
|
|
||||||
// ------------------------------------------------------------------ equipment
|
// ------------------------------------------------------------------ equipment
|
||||||
|
|
||||||
// Tools and attachments sell back for equipmentResale.factor (below 1) of their price in
|
// Tools sell back for equipmentResale.factor (below 1) of their price in credits: the
|
||||||
// credits: a tool for its store price plus the upgrades it has had. Refined parts used by
|
// store price plus the track levels it has had (tracks.go). Refined parts used by
|
||||||
// upgrades are not refunded.
|
// upgrades are not refunded.
|
||||||
type EquipmentResale struct {
|
type EquipmentResale struct {
|
||||||
Factor float64 `json:"factor"`
|
Factor float64 `json:"factor"`
|
||||||
@@ -303,19 +303,11 @@ type EquipmentResale struct {
|
|||||||
func resaleOf(price float64) float64 { return math.Floor(price * cfg().EquipmentResale.Factor) }
|
func resaleOf(price float64) float64 { return math.Floor(price * cfg().EquipmentResale.Factor) }
|
||||||
|
|
||||||
// ToolResale is what a tool sells for: its price and its upgrades' prices, × the factor.
|
// ToolResale is what a tool sells for: its price and its upgrades' prices, × the factor.
|
||||||
func (item *StoreItem) ToolResale(tier int) float64 {
|
func (item *StoreItem) ToolResale(lv Levels) float64 {
|
||||||
paid := item.Price
|
return resaleOf(item.Price + item.upgradesPrice(lv))
|
||||||
for _, u := range item.Upgrades {
|
|
||||||
if u.Tier <= tier {
|
|
||||||
paid += u.Price
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resaleOf(paid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Attachment) Resale() float64 { return resaleOf(a.Price) }
|
// sellTool sells one of the player's idle tools.
|
||||||
|
|
||||||
// sellTool sells one of the player's idle tools. Its attachments go back to the inventory.
|
|
||||||
func sellTool(ctx context.Context, db *pgxpool.Pool, userID, toolID string) (string, float64, error) {
|
func sellTool(ctx context.Context, db *pgxpool.Pool, userID, toolID string) (string, float64, error) {
|
||||||
var name string
|
var name string
|
||||||
var earned float64
|
var earned float64
|
||||||
@@ -329,17 +321,19 @@ func sellTool(ctx context.Context, db *pgxpool.Pool, userID, toolID string) (str
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var storeItemID, tier int
|
var storeItemID int
|
||||||
var price, upgrades float64
|
var lv Levels
|
||||||
err = tx.QueryRow(ctx, `
|
err = tx.QueryRow(ctx, `
|
||||||
select inventory_item.store_item_id, inventory_item.tier, store_item.name, store_item.price,
|
select inventory_item.store_item_id, inventory_item.yield_level, inventory_item.cycle_level, store_item.name
|
||||||
(select coalesce(sum(price), 0) from upgrade_item
|
|
||||||
where store_item_id = inventory_item.store_item_id and tier <= inventory_item.tier)
|
|
||||||
from inventory_item join store_item on store_item.id = inventory_item.store_item_id
|
from inventory_item join store_item on store_item.id = inventory_item.store_item_id
|
||||||
where inventory_item.id = $1`, toolID).Scan(&storeItemID, &tier, &name, &price, &upgrades)
|
where inventory_item.id = $1`, toolID).Scan(&storeItemID, &lv.Yield, &lv.Cycle, &name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
item := configItem(storeItemID)
|
||||||
|
if item == nil {
|
||||||
|
return gameErr("That tool can't be sold")
|
||||||
|
}
|
||||||
var tools int
|
var tools int
|
||||||
err = tx.QueryRow(ctx, "select count(*) from inventory_item where user_id = $1", userID).Scan(&tools)
|
err = tx.QueryRow(ctx, "select count(*) from inventory_item where user_id = $1", userID).Scan(&tools)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -366,11 +360,7 @@ func sellTool(ctx context.Context, db *pgxpool.Pool, userID, toolID string) (str
|
|||||||
return gameErr("Claim this drill's haul first")
|
return gameErr("Claim this drill's haul first")
|
||||||
}
|
}
|
||||||
|
|
||||||
earned = resaleOf(price + upgrades)
|
earned = item.ToolResale(lv)
|
||||||
tag, err := tx.Exec(ctx, "update attachment set inventory_item_id = null where inventory_item_id = $1", toolID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec(ctx, "delete from inventory_item where id = $1", toolID); err != nil {
|
if _, err := tx.Exec(ctx, "delete from inventory_item where id = $1", toolID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -380,36 +370,7 @@ func sellTool(ctx context.Context, db *pgxpool.Pool, userID, toolID string) (str
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return logEvent(ctx, tx, userID, "sell_tool", map[string]any{"tool": toolID, "item": storeItemID,
|
return logEvent(ctx, tx, userID, "sell_tool", map[string]any{"tool": toolID, "item": storeItemID,
|
||||||
"tier": tier, "credits": earned, "attachments": tag.RowsAffected()})
|
"yieldLevel": lv.Yield, "cycleLevel": lv.Cycle, "credits": earned})
|
||||||
})
|
})
|
||||||
return name, earned, err
|
return name, earned, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// sellAttachment sells one of the player's spare (unequipped) attachments of a kind.
|
|
||||||
func sellAttachment(ctx context.Context, db *pgxpool.Pool, userID, name string) (float64, error) {
|
|
||||||
a := cfg().attachment(name)
|
|
||||||
if a == nil {
|
|
||||||
return 0, gameErr("Unknown attachment")
|
|
||||||
}
|
|
||||||
earned := a.Resale()
|
|
||||||
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
|
||||||
tag, err := tx.Exec(ctx, `
|
|
||||||
delete from attachment where id = (
|
|
||||||
select id from attachment
|
|
||||||
where user_id = $1 and item = $2 and inventory_item_id is null
|
|
||||||
limit 1 for update skip locked)`, userID, a.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if tag.RowsAffected() == 0 {
|
|
||||||
return gameErr("You have no spare %s", a.Name)
|
|
||||||
}
|
|
||||||
_, err = tx.Exec(ctx,
|
|
||||||
"update bank_account set balance = balance + $2 where user_id = $1", userID, earned)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return logEvent(ctx, tx, userID, "sell_attachment", map[string]any{"item": a.Name, "credits": earned})
|
|
||||||
})
|
|
||||||
return earned, err
|
|
||||||
}
|
|
||||||
+7
-30
@@ -384,6 +384,7 @@ type actionResult struct {
|
|||||||
View View // the view to show next (the request's, unless the action moves on)
|
View View // the view to show next (the request's, unless the action moves on)
|
||||||
BoughtTool string // a tool just bought (the buy modal)
|
BoughtTool string // a tool just bought (the buy modal)
|
||||||
Upgraded string // a tool just upgraded (its toast)
|
Upgraded string // a tool just upgraded (its toast)
|
||||||
|
Track string // the track it upgraded (yield, cycle)
|
||||||
Finished []string // moons a claim mined out
|
Finished []string // moons a claim mined out
|
||||||
SaleCredits float64 // what a market sale earned (the sell ticket's payout effect)
|
SaleCredits float64 // what a market sale earned (the sell ticket's payout effect)
|
||||||
}
|
}
|
||||||
@@ -399,7 +400,7 @@ var errForbidden = errors.New("missing permission")
|
|||||||
// unless the action moves on. new-game only passes the gate; the caller creates the player.
|
// unless the action moves on. new-game only passes the gate; the caller creates the player.
|
||||||
func (s *server) runAction(ctx context.Context, userID, action string, get func(string) string) (actionResult, error) {
|
func (s *server) runAction(ctx context.Context, userID, action string, get func(string) string) (actionResult, error) {
|
||||||
view := viewFromForm(get)
|
view := viewFromForm(get)
|
||||||
var message, boughtTool, upgraded string
|
var message, boughtTool, upgraded, track string
|
||||||
var finished []string
|
var finished []string
|
||||||
var saleCredits float64
|
var saleCredits float64
|
||||||
var err error
|
var err error
|
||||||
@@ -430,12 +431,6 @@ func (s *server) runAction(ctx context.Context, userID, action string, get func(
|
|||||||
if name, earned, err = sellTool(ctx, s.db, userID, get("tool")); err == nil {
|
if name, earned, err = sellTool(ctx, s.db, userID, get("tool")); err == nil {
|
||||||
message = fmt.Sprintf("Sold %s for %s", name, formatCredits(earned))
|
message = fmt.Sprintf("Sold %s for %s", name, formatCredits(earned))
|
||||||
}
|
}
|
||||||
case "sell-attachment":
|
|
||||||
item := get("item")
|
|
||||||
var earned float64
|
|
||||||
if earned, err = sellAttachment(ctx, s.db, userID, item); err == nil {
|
|
||||||
message = fmt.Sprintf("Sold %s for %s", item, formatCredits(earned))
|
|
||||||
}
|
|
||||||
case "buy-tool":
|
case "buy-tool":
|
||||||
id, _ := strconv.Atoi(get("id"))
|
id, _ := strconv.Atoi(get("id"))
|
||||||
if boughtTool, err = buyTool(ctx, s.db, userID, id); err == nil {
|
if boughtTool, err = buyTool(ctx, s.db, userID, id); err == nil {
|
||||||
@@ -443,7 +438,9 @@ func (s *server) runAction(ctx context.Context, userID, action string, get func(
|
|||||||
}
|
}
|
||||||
case "upgrade":
|
case "upgrade":
|
||||||
var tool string
|
var tool string
|
||||||
if tool, err = upgradeTool(ctx, s.db, userID, get("tool")); err == nil {
|
// Raises one of the tool's upgrade tracks (track=yield|cycle) a level (tracks.go).
|
||||||
|
track = get("track")
|
||||||
|
if tool, err = upgradeTool(ctx, s.db, userID, get("tool"), track); err == nil {
|
||||||
message, upgraded = "Tool upgraded", tool
|
message, upgraded = "Tool upgraded", tool
|
||||||
view.Drill = followTool(view.Drill, get("tool"), tool)
|
view.Drill = followTool(view.Drill, get("tool"), tool)
|
||||||
}
|
}
|
||||||
@@ -548,26 +545,6 @@ func (s *server) runAction(ctx context.Context, userID, action string, get func(
|
|||||||
if n, err = upgradeRefineryBatch(ctx, s.db, userID); err == nil {
|
if n, err = upgradeRefineryBatch(ctx, s.db, userID); err == nil {
|
||||||
message = fmt.Sprintf("Refinery batches up to %d", n)
|
message = fmt.Sprintf("Refinery batches up to %d", n)
|
||||||
}
|
}
|
||||||
case "buy-attachment":
|
|
||||||
item := get("item")
|
|
||||||
if err = buyAttachment(ctx, s.db, userID, item); err == nil {
|
|
||||||
message = item + " added to your inventory"
|
|
||||||
}
|
|
||||||
case "equip", "unequip":
|
|
||||||
// Puts an attachment (item, by name) on a tool (to), or takes it off.
|
|
||||||
// A busy tool's idle copy may get it instead; the panel follows that one.
|
|
||||||
item, tool := get("item"), get("to")
|
|
||||||
var used string
|
|
||||||
if action == "equip" {
|
|
||||||
used, err = equipAttachment(ctx, s.db, userID, item, tool)
|
|
||||||
message = item + " equipped"
|
|
||||||
} else {
|
|
||||||
used, err = unequipAttachment(ctx, s.db, userID, item, tool)
|
|
||||||
message = item + " unequipped"
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
view.Drill = followTool(view.Drill, tool, used)
|
|
||||||
}
|
|
||||||
case "hire":
|
case "hire":
|
||||||
name := get("advisor")
|
name := get("advisor")
|
||||||
if err = hireAdvisor(ctx, s.db, userID, name); err == nil {
|
if err = hireAdvisor(ctx, s.db, userID, name); err == nil {
|
||||||
@@ -623,7 +600,7 @@ func (s *server) runAction(ctx context.Context, userID, action string, get func(
|
|||||||
return actionResult{}, err
|
return actionResult{}, err
|
||||||
}
|
}
|
||||||
return actionResult{Action: action, Err: err, Message: message, View: view, BoughtTool: boughtTool,
|
return actionResult{Action: action, Err: err, Message: message, View: view, BoughtTool: boughtTool,
|
||||||
Upgraded: upgraded, Finished: finished, SaleCredits: saleCredits}, nil
|
Upgraded: upgraded, Track: track, Finished: finished, SaleCredits: saleCredits}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// toast is the action's toast, for the state after it.
|
// toast is the action's toast, for the state after it.
|
||||||
@@ -633,7 +610,7 @@ func (res actionResult) toast(state *State) *Toast {
|
|||||||
}
|
}
|
||||||
toast := &Toast{Message: res.Message, Kind: res.Action, Finished: strings.Join(res.Finished, ","), Credits: res.SaleCredits}
|
toast := &Toast{Message: res.Message, Kind: res.Action, Finished: strings.Join(res.Finished, ","), Credits: res.SaleCredits}
|
||||||
if t := state.tool(res.Upgraded); t != nil {
|
if t := state.tool(res.Upgraded); t != nil {
|
||||||
toast.Message = state.upgradedText(*t)
|
toast.Message = state.upgradedText(*t, res.Track)
|
||||||
}
|
}
|
||||||
return toast
|
return toast
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-80
@@ -14,7 +14,8 @@ import (
|
|||||||
const (
|
const (
|
||||||
// A bigger tool should earn at least this many times the previous one per hour.
|
// A bigger tool should earn at least this many times the previous one per hour.
|
||||||
minRateStep = 1.2
|
minRateStep = 1.2
|
||||||
// An upgrade shouldn't take more than this many times as long to pay back as its tool.
|
// An upgrade (a track level) shouldn't take more than this many times as long to pay
|
||||||
|
// back as its tool.
|
||||||
maxUpgradePayback = 2.0
|
maxUpgradePayback = 2.0
|
||||||
// Payback time shouldn't jump by more than this between consecutive tools.
|
// Payback time shouldn't jump by more than this between consecutive tools.
|
||||||
maxPaybackJump = 4.0
|
maxPaybackJump = 4.0
|
||||||
@@ -88,15 +89,18 @@ func (rc ReachCaps) Yield(amount float64, reach int) float64 {
|
|||||||
return (r.sums[i] + amount*float64(n-i)) / float64(n)
|
return (r.sums[i] + amount*float64(n-i)) / float64(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TierStat struct {
|
// LevelStat is a level of a tool's upgrade track, the other track at 0.
|
||||||
Tier int
|
type LevelStat struct {
|
||||||
|
Track string // yield, cycle
|
||||||
|
Level int
|
||||||
Price float64
|
Price float64
|
||||||
PartsValue float64 // sell value of the refined parts it uses up
|
PartsValue float64 // sell value of the refined parts it uses up
|
||||||
Yield float64
|
Yield float64
|
||||||
|
Secs float64
|
||||||
Reach int
|
Reach int
|
||||||
Eff float64 // what a claim gets on average from a fresh moon (ReachCaps)
|
Eff float64 // what a claim gets on average from a fresh moon (ReachCaps)
|
||||||
CreditsPerHour float64 // at Eff
|
CreditsPerHour float64 // at Eff
|
||||||
Payback Hours // for the upgrade's extra income to repay its price plus parts
|
Payback Hours // for the level's extra income (over the one below) to repay its price plus parts
|
||||||
TooBig bool // yield above what its reach can hold (Capacity)
|
TooBig bool // yield above what its reach can hold (Capacity)
|
||||||
Capacity float64
|
Capacity float64
|
||||||
}
|
}
|
||||||
@@ -118,7 +122,7 @@ type ToolStat struct {
|
|||||||
TooBig bool
|
TooBig bool
|
||||||
RatioLow bool
|
RatioLow bool
|
||||||
JumpHigh bool
|
JumpHigh bool
|
||||||
Tiers []TierStat
|
Levels []LevelStat // both tracks, yield first
|
||||||
}
|
}
|
||||||
|
|
||||||
type Analysis struct {
|
type Analysis struct {
|
||||||
@@ -175,7 +179,7 @@ func avgWells(c *GameConfig, g *MoonGrade) float64 {
|
|||||||
// closest to 6.5 claims (each of its reach wells gets a share of a claim).
|
// closest to 6.5 claims (each of its reach wells gets a share of a claim).
|
||||||
func suitedGrade(c *GameConfig, item *StoreItem) *MoonGrade {
|
func suitedGrade(c *GameConfig, item *StoreItem) *MoonGrade {
|
||||||
best, bestD := &c.MoonGrades[0], math.Inf(1)
|
best, bestD := &c.MoonGrades[0], math.Inf(1)
|
||||||
perWell := item.ClaimAmount / float64(item.ToolReach(0))
|
perWell := item.ClaimAmount / float64(item.ToolReach())
|
||||||
for i := range c.MoonGrades {
|
for i := range c.MoonGrades {
|
||||||
g := &c.MoonGrades[i]
|
g := &c.MoonGrades[i]
|
||||||
if d := math.Abs(math.Log(g.MeanSupply() / max(1e-9, perWell) / 6.5)); d < bestD {
|
if d := math.Abs(math.Log(g.MeanSupply() / max(1e-9, perWell) / 6.5)); d < bestD {
|
||||||
@@ -195,11 +199,11 @@ func configItemIn(c *GameConfig, id int) *StoreItem {
|
|||||||
return &StoreItem{ClaimAmount: 1}
|
return &StoreItem{ClaimAmount: 1}
|
||||||
}
|
}
|
||||||
|
|
||||||
func perHour(units float64, secs int) float64 {
|
func perHour(units, secs float64) float64 {
|
||||||
if secs <= 0 {
|
if secs <= 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return units * 3600 / float64(secs)
|
return units * 3600 / secs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hours is a duration in hours; Inf for never.
|
// Hours is a duration in hours; Inf for never.
|
||||||
@@ -263,9 +267,9 @@ func analyze(c *GameConfig, errs []string, unsaved int) *Analysis {
|
|||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
caps := reachCaps(c, suitedGrade(c, &item))
|
caps := reachCaps(c, suitedGrade(c, &item))
|
||||||
t := ToolStat{ID: item.ID, Name: item.Name, Price: item.Price, Claim: item.ClaimAmount,
|
t := ToolStat{ID: item.ID, Name: item.Name, Price: item.Price, Claim: item.ClaimAmount,
|
||||||
Secs: item.CompletionTimeInSecs, Reach: item.ToolReach(0)}
|
Secs: item.CompletionTimeInSecs, Reach: item.ToolReach()}
|
||||||
t.Eff = caps.Yield(item.ClaimAmount, t.Reach)
|
t.Eff = caps.Yield(item.ClaimAmount, t.Reach)
|
||||||
t.UnitsPerHour = perHour(t.Eff, item.CompletionTimeInSecs)
|
t.UnitsPerHour = perHour(t.Eff, float64(item.CompletionTimeInSecs))
|
||||||
t.CreditsPerHour = t.UnitsPerHour * a.AvgPrice
|
t.CreditsPerHour = t.UnitsPerHour * a.AvgPrice
|
||||||
t.Payback = hours(item.Price, t.CreditsPerHour)
|
t.Payback = hours(item.Price, t.CreditsPerHour)
|
||||||
maxWell := wellMax(item.ID)
|
maxWell := wellMax(item.ID)
|
||||||
@@ -283,33 +287,34 @@ func analyze(c *GameConfig, errs []string, unsaved int) *Analysis {
|
|||||||
t.JumpHigh = t.PaybackJump > maxPaybackJump
|
t.JumpHigh = t.PaybackJump > maxPaybackJump
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, u := range item.Upgrades {
|
for _, kind := range trackKinds {
|
||||||
yield, reach := item.ClaimAmount+u.ClaimBoost, item.ToolReach(u.Tier)
|
|
||||||
eff := caps.Yield(yield, reach)
|
|
||||||
rate := perHour(eff, item.ToolSecs(u.Tier)) * a.AvgPrice
|
|
||||||
prevRate := t.CreditsPerHour
|
prevRate := t.CreditsPerHour
|
||||||
if len(t.Tiers) > 0 {
|
for n, u := range item.Track(kind).Levels {
|
||||||
prevRate = t.Tiers[len(t.Tiers)-1].CreditsPerHour
|
lv := Levels{}.With(kind, n+1)
|
||||||
}
|
yield, secs, reach := item.ToolYield(lv), item.ToolSecs(lv), item.ToolReach()
|
||||||
|
eff := caps.Yield(yield, reach)
|
||||||
|
rate := perHour(eff, secs) * a.AvgPrice
|
||||||
parts := partsValue(c, u.Parts)
|
parts := partsValue(c, u.Parts)
|
||||||
t.Tiers = append(t.Tiers, TierStat{Tier: u.Tier, Price: u.Price, PartsValue: parts, Yield: yield,
|
t.Levels = append(t.Levels, LevelStat{Track: kind, Level: n + 1, Price: u.Price, PartsValue: parts, Yield: yield,
|
||||||
Reach: reach, Eff: eff, CreditsPerHour: rate, Payback: hours(u.Price+parts, rate-prevRate),
|
Secs: secs, Reach: reach, Eff: eff, CreditsPerHour: rate, Payback: hours(u.Price+parts, rate-prevRate),
|
||||||
Capacity: capacity(reach), TooBig: yield > capacity(reach)})
|
Capacity: capacity(reach), TooBig: yield > capacity(reach)})
|
||||||
|
prevRate = rate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// An upgrade should be worth its price next to buying the next tool, and pay back
|
// An upgrade should be worth its price next to buying the next tool, and pay back
|
||||||
// about as fast as the tool itself.
|
// about as fast as the tool itself.
|
||||||
for _, u := range t.Tiers {
|
for _, u := range t.Levels {
|
||||||
if i+1 < len(items) {
|
if i+1 < len(items) {
|
||||||
next := &items[i+1]
|
next := &items[i+1]
|
||||||
nextRate := perHour(caps.Yield(next.ClaimAmount, next.ToolReach(0)), next.CompletionTimeInSecs) * a.AvgPrice
|
nextRate := perHour(caps.Yield(next.ClaimAmount, next.ToolReach()), float64(next.CompletionTimeInSecs)) * a.AvgPrice
|
||||||
if u.Price+u.PartsValue > next.Price && u.CreditsPerHour < nextRate {
|
if u.Price+u.PartsValue > next.Price && u.CreditsPerHour < nextRate {
|
||||||
a.warn(fmt.Sprintf("item:%d", item.ID), fmt.Sprintf("%s tier %d costs %s, more than %s (%s), and earns less.",
|
a.warn(fmt.Sprintf("item:%d", item.ID), fmt.Sprintf("%s %s level %d costs %s, more than %s (%s), and earns less.",
|
||||||
item.Name, u.Tier, formatCredits(u.Price+u.PartsValue), next.Name, formatCredits(next.Price)))
|
item.Name, u.Track, u.Level, formatCredits(u.Price+u.PartsValue), next.Name, formatCredits(next.Price)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if u.Payback > maxUpgradePayback*t.Payback {
|
if u.Payback > maxUpgradePayback*t.Payback {
|
||||||
a.warn(fmt.Sprintf("item:%d", item.ID), fmt.Sprintf("%s tier %d pays back in %s, over %.0f× the tool itself (%s).",
|
a.warn(fmt.Sprintf("item:%d", item.ID), fmt.Sprintf("%s %s level %d pays back in %s, over %.0f× the tool itself (%s).",
|
||||||
item.Name, u.Tier, u.Payback, maxUpgradePayback, t.Payback))
|
item.Name, u.Track, u.Level, u.Payback, maxUpgradePayback, t.Payback))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
key := fmt.Sprintf("item:%d", item.ID)
|
key := fmt.Sprintf("item:%d", item.ID)
|
||||||
@@ -317,10 +322,10 @@ func analyze(c *GameConfig, errs []string, unsaved int) *Analysis {
|
|||||||
a.warn(key, fmt.Sprintf("%s claims %s, more than %s can hold (%s).",
|
a.warn(key, fmt.Sprintf("%s claims %s, more than %s can hold (%s).",
|
||||||
item.Name, formatMoney(item.ClaimAmount), reachText(t.Reach), formatMoney(t.Capacity)))
|
item.Name, formatMoney(item.ClaimAmount), reachText(t.Reach), formatMoney(t.Capacity)))
|
||||||
} else {
|
} else {
|
||||||
for _, u := range t.Tiers {
|
for _, u := range t.Levels {
|
||||||
if u.TooBig {
|
if u.TooBig {
|
||||||
a.warn(key, fmt.Sprintf("%s at tier %d claims %s, more than %s can hold (%s).",
|
a.warn(key, fmt.Sprintf("%s at yield level %d claims %s, more than %s can hold (%s).",
|
||||||
item.Name, u.Tier, formatMoney(u.Yield), reachText(u.Reach), formatMoney(u.Capacity)))
|
item.Name, u.Level, formatMoney(u.Yield), reachText(u.Reach), formatMoney(u.Capacity)))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,7 +403,7 @@ type SimResult struct {
|
|||||||
Hours int
|
Hours int
|
||||||
Reaction int // seconds between the player's actions
|
Reaction int // seconds between the player's actions
|
||||||
Tools []SimTool
|
Tools []SimTool
|
||||||
Attachments []SimBuy // equipped, by kind
|
Upgrades int // track levels bought
|
||||||
Advisors []SimBuy // hired
|
Advisors []SimBuy // hired
|
||||||
Seats int // advisor seats at the end
|
Seats int // advisor seats at the end
|
||||||
Moons []SimMoon
|
Moons []SimMoon
|
||||||
@@ -409,7 +414,7 @@ type SimResult struct {
|
|||||||
CappedAt SimTime
|
CappedAt SimTime
|
||||||
}
|
}
|
||||||
|
|
||||||
// SimBuy is an attachment or advisor the sim player bought: when first, and how many.
|
// SimBuy is an advisor the sim player bought: when first, and how many.
|
||||||
type SimBuy struct {
|
type SimBuy struct {
|
||||||
Name string
|
Name string
|
||||||
BoughtAt SimTime // -1 for never
|
BoughtAt SimTime // -1 for never
|
||||||
@@ -418,8 +423,7 @@ type SimBuy struct {
|
|||||||
|
|
||||||
type simTool struct {
|
type simTool struct {
|
||||||
item int // index into the config's store items
|
item int // index into the config's store items
|
||||||
tier int
|
lv Levels
|
||||||
mods []string // attachments equipped
|
|
||||||
busy bool
|
busy bool
|
||||||
doneAt int
|
doneAt int
|
||||||
amount float64
|
amount float64
|
||||||
@@ -428,8 +432,8 @@ type simTool struct {
|
|||||||
// simulate plays a greedy, active player: starting config, sells everything at the average
|
// simulate plays a greedy, active player: starting config, sells everything at the average
|
||||||
// price, and every simReaction seconds claims and redeploys every finished drill, buys a
|
// price, and every simReaction seconds claims and redeploys every finished drill, buys a
|
||||||
// moon when its wells can't take two more rounds from every drill, and otherwise buys
|
// moon when its wells can't take two more rounds from every drill, and otherwise buys
|
||||||
// what breaks even soonest (time to save up plus payback): a tool, an upgrade, an
|
// what breaks even soonest (time to save up plus payback): a tool, a track level for one
|
||||||
// attachment for one tool or an advisor (with a seat if needed), keeping enough for the next moon.
|
// tool or an advisor (with a seat if needed), keeping enough for the next moon.
|
||||||
func simulate(c *GameConfig) *SimResult {
|
func simulate(c *GameConfig) *SimResult {
|
||||||
price := avgPrice(c)
|
price := avgPrice(c)
|
||||||
rng := rand.New(rand.NewPCG(simSeed[0], simSeed[1]))
|
rng := rand.New(rand.NewPCG(simSeed[0], simSeed[1]))
|
||||||
@@ -440,29 +444,26 @@ func simulate(c *GameConfig) *SimResult {
|
|||||||
itemIndex[item.ID] = i
|
itemIndex[item.ID] = i
|
||||||
res.Tools = append(res.Tools, SimTool{Name: item.Name, Price: item.Price, AffordAt: -1, BoughtAt: -1})
|
res.Tools = append(res.Tools, SimTool{Name: item.Name, Price: item.Price, AffordAt: -1, BoughtAt: -1})
|
||||||
}
|
}
|
||||||
for _, m := range c.Attachments.Items {
|
|
||||||
res.Attachments = append(res.Attachments, SimBuy{Name: m.Name, BoughtAt: -1})
|
|
||||||
}
|
|
||||||
for _, a := range c.Advisors.Items {
|
for _, a := range c.Advisors.Items {
|
||||||
res.Advisors = append(res.Advisors, SimBuy{Name: a.Name, BoughtAt: -1})
|
res.Advisors = append(res.Advisors, SimBuy{Name: a.Name, BoughtAt: -1})
|
||||||
}
|
}
|
||||||
var advisors []string // hired
|
var advisors []string // hired
|
||||||
// A tool's cycle and yield with its attachments and the given advisors (bonus.go). A drill
|
// A tool's cycle and yield at its levels with the given advisors (bonus.go). A drill
|
||||||
// that finishes between the player's actions waits for the next one.
|
// that finishes between the player's actions waits for the next one.
|
||||||
stats := func(item, tier int, mods, adv []string) (int, float64) {
|
stats := func(item int, lv Levels, adv []string) (int, float64) {
|
||||||
it := &c.StoreItems[item]
|
it := &c.StoreItems[item]
|
||||||
secs, y := drillStats(it.ToolSecs(tier), it.ToolYield(tier), c.bonusesFor(adv, mods), 1, 1)
|
secs, y := drillStats(it.ToolSecs(lv), it.ToolYield(lv), c.bonusesFor(adv), 1, 1)
|
||||||
return (secs + simReaction - 1) / simReaction * simReaction, y
|
return int(math.Ceil(secs/simReaction-1e-9)) * simReaction, y
|
||||||
}
|
}
|
||||||
// rate is a tool's credits per hour, with what its reach gets from a fresh moon.
|
// rate is a tool's credits per hour, with what its reach gets from a fresh moon.
|
||||||
capsBy := map[string]ReachCaps{}
|
capsBy := map[string]ReachCaps{}
|
||||||
rate := func(item, tier int, mods, adv []string) float64 {
|
rate := func(item int, lv Levels, adv []string) float64 {
|
||||||
secs, y := stats(item, tier, mods, adv)
|
secs, y := stats(item, lv, adv)
|
||||||
g := suitedGrade(c, &c.StoreItems[item])
|
g := suitedGrade(c, &c.StoreItems[item])
|
||||||
if capsBy[g.Grade] == nil {
|
if capsBy[g.Grade] == nil {
|
||||||
capsBy[g.Grade] = reachCaps(c, g)
|
capsBy[g.Grade] = reachCaps(c, g)
|
||||||
}
|
}
|
||||||
return perHour(capsBy[g.Grade].Yield(y, c.StoreItems[item].ToolReach(tier)), secs) * price * c.bonusesFor(adv, nil).Sell
|
return perHour(capsBy[g.Grade].Yield(y, c.StoreItems[item].ToolReach()), float64(secs)) * price * c.bonusesFor(adv).Sell
|
||||||
}
|
}
|
||||||
// The grade of the next moon: the dearest one the player could buy twice.
|
// The grade of the next moon: the dearest one the player could buy twice.
|
||||||
nextGrade := func(credits float64) *MoonGrade {
|
nextGrade := func(credits float64) *MoonGrade {
|
||||||
@@ -474,9 +475,9 @@ func simulate(c *GameConfig) *SimResult {
|
|||||||
}
|
}
|
||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
reach := func(t *simTool) int { return c.StoreItems[t.item].ToolReach(t.tier) }
|
reach := func(t *simTool) int { return c.StoreItems[t.item].ToolReach() }
|
||||||
yield := func(t *simTool) float64 { _, y := stats(t.item, t.tier, t.mods, advisors); return y }
|
yield := func(t *simTool) float64 { _, y := stats(t.item, t.lv, advisors); return y }
|
||||||
cycle := func(t *simTool) int { secs, _ := stats(t.item, t.tier, t.mods, advisors); return secs }
|
cycle := func(t *simTool) int { secs, _ := stats(t.item, t.lv, advisors); return secs }
|
||||||
|
|
||||||
credits := c.StartingCredits + c.StartingResources*price*float64(len(c.ResourcePrices))
|
credits := c.StartingCredits + c.StartingResources*price*float64(len(c.ResourcePrices))
|
||||||
earned, earnedAtSnap, snapAt, moonsAtSnap := 0.0, 0.0, 0, 0
|
earned, earnedAtSnap, snapAt, moonsAtSnap := 0.0, 0.0, 0, 0
|
||||||
@@ -493,7 +494,7 @@ func simulate(c *GameConfig) *SimResult {
|
|||||||
income := func(adv []string) float64 {
|
income := func(adv []string) float64 {
|
||||||
sum := 0.0
|
sum := 0.0
|
||||||
for i := range tools {
|
for i := range tools {
|
||||||
sum += rate(tools[i].item, tools[i].tier, tools[i].mods, adv)
|
sum += rate(tools[i].item, tools[i].lv, adv)
|
||||||
}
|
}
|
||||||
return sum
|
return sum
|
||||||
}
|
}
|
||||||
@@ -526,7 +527,7 @@ func simulate(c *GameConfig) *SimResult {
|
|||||||
act := t%simReaction == 0
|
act := t%simReaction == 0
|
||||||
// Claim finished drills (and sell at once, with the advisors' sell bonus and selling
|
// Claim finished drills (and sell at once, with the advisors' sell bonus and selling
|
||||||
// pressure, spread evenly over the resources).
|
// pressure, spread evenly over the resources).
|
||||||
sell := c.bonusesFor(advisors, nil).Sell
|
sell := c.bonusesFor(advisors).Sell
|
||||||
if act {
|
if act {
|
||||||
pressure = c.MarketPressure.Decay(pressure, simReaction)
|
pressure = c.MarketPressure.Decay(pressure, simReaction)
|
||||||
units := 0.0
|
units := 0.0
|
||||||
@@ -599,17 +600,17 @@ func simulate(c *GameConfig) *SimResult {
|
|||||||
event(t, "moon #%d (%s)", moons, formatCredits(moonPrice))
|
event(t, "moon #%d (%s)", moons, formatCredits(moonPrice))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Soonest break-even (time to save up plus payback): a new tool; the next tier or
|
// Soonest break-even (time to save up plus payback): a new tool; the next level of
|
||||||
// an attachment for one owned tool of each kind; or an advisor.
|
// either track for one owned tool of each kind; or an advisor.
|
||||||
now := income(advisors)
|
now := income(advisors)
|
||||||
breakEven := func(cost, gain float64) Hours {
|
breakEven := func(cost, gain float64) Hours {
|
||||||
return hours(max(0, cost-credits), now) + hours(cost, gain)
|
return hours(max(0, cost-credits), now) + hours(cost, gain)
|
||||||
}
|
}
|
||||||
type choice struct {
|
type choice struct {
|
||||||
kind string // tool, upgrade, attachment, advisor
|
kind string // tool, upgrade, advisor
|
||||||
item int // store item index (tool)
|
item int // store item index (tool)
|
||||||
tool int // index into tools (upgrade, attachment)
|
tool int // index into tools (upgrade)
|
||||||
name string // attachment or advisor
|
name string // advisor; upgrade: the track
|
||||||
seat float64
|
seat float64
|
||||||
cost float64
|
cost float64
|
||||||
payback Hours
|
payback Hours
|
||||||
@@ -622,7 +623,7 @@ func simulate(c *GameConfig) *SimResult {
|
|||||||
}
|
}
|
||||||
if len(tools) < simMaxTools {
|
if len(tools) < simMaxTools {
|
||||||
for i, item := range c.StoreItems {
|
for i, item := range c.StoreItems {
|
||||||
consider(choice{kind: "tool", item: i, cost: item.Price}, rate(i, 0, nil, advisors))
|
consider(choice{kind: "tool", item: i, cost: item.Price}, rate(i, Levels{}, advisors))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
capped(t)
|
capped(t)
|
||||||
@@ -630,27 +631,19 @@ func simulate(c *GameConfig) *SimResult {
|
|||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for i := range tools {
|
for i := range tools {
|
||||||
tl := &tools[i]
|
tl := &tools[i]
|
||||||
key := fmt.Sprint(tl.item, tl.tier, tl.mods)
|
key := fmt.Sprint(tl.item, tl.lv)
|
||||||
if seen[key] {
|
if seen[key] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
item := &c.StoreItems[tl.item]
|
item := &c.StoreItems[tl.item]
|
||||||
cur := rate(tl.item, tl.tier, tl.mods, advisors)
|
cur := rate(tl.item, tl.lv, advisors)
|
||||||
if up := item.NextUpgrade(tl.tier); up != nil {
|
for _, kind := range trackKinds {
|
||||||
consider(choice{kind: "upgrade", item: tl.item, tool: i, cost: up.Price + partsValue(c, up.Parts)},
|
n := tl.lv.Of(kind) + 1
|
||||||
rate(tl.item, tl.tier+1, tl.mods, advisors)-cur)
|
if up := item.TrackLevel(kind, n); up != nil {
|
||||||
|
consider(choice{kind: "upgrade", item: tl.item, tool: i, name: kind, cost: up.Price + partsValue(c, up.Parts)},
|
||||||
|
rate(tl.item, tl.lv.With(kind, n), advisors)-cur)
|
||||||
}
|
}
|
||||||
if len(tl.mods) >= c.Attachments.SlotsPerTool {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, m := range c.Attachments.Items {
|
|
||||||
if slices.Contains(tl.mods, m.Name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mods := append(slices.Clone(tl.mods), m.Name)
|
|
||||||
consider(choice{kind: "attachment", item: tl.item, tool: i, name: m.Name, cost: m.Price},
|
|
||||||
rate(tl.item, tl.tier, mods, advisors)-cur)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, a := range c.Advisors.Items {
|
for _, a := range c.Advisors.Items {
|
||||||
@@ -674,8 +667,11 @@ func simulate(c *GameConfig) *SimResult {
|
|||||||
credits -= best.cost
|
credits -= best.cost
|
||||||
switch best.kind {
|
switch best.kind {
|
||||||
case "upgrade":
|
case "upgrade":
|
||||||
tools[best.tool].tier++
|
lv := tools[best.tool].lv
|
||||||
event(t, "upgrade %s to tier %d (%s)", c.StoreItems[best.item].Name, tools[best.tool].tier, formatCredits(best.cost))
|
tools[best.tool].lv = lv.With(best.name, lv.Of(best.name)+1)
|
||||||
|
res.Upgrades++
|
||||||
|
event(t, "upgrade %s %s to level %d (%s)", c.StoreItems[best.item].Name, best.name,
|
||||||
|
tools[best.tool].lv.Of(best.name), formatCredits(best.cost))
|
||||||
case "tool":
|
case "tool":
|
||||||
tools = append(tools, simTool{item: best.item})
|
tools = append(tools, simTool{item: best.item})
|
||||||
if res.Tools[best.item].BoughtAt < 0 {
|
if res.Tools[best.item].BoughtAt < 0 {
|
||||||
@@ -683,14 +679,6 @@ func simulate(c *GameConfig) *SimResult {
|
|||||||
}
|
}
|
||||||
res.Tools[best.item].Count++
|
res.Tools[best.item].Count++
|
||||||
event(t, "buy %s (%s)", c.StoreItems[best.item].Name, formatCredits(best.cost))
|
event(t, "buy %s (%s)", c.StoreItems[best.item].Name, formatCredits(best.cost))
|
||||||
case "attachment":
|
|
||||||
tools[best.tool].mods = append(slices.Clone(tools[best.tool].mods), best.name)
|
|
||||||
for i := range res.Attachments {
|
|
||||||
if res.Attachments[i].Name == best.name {
|
|
||||||
bought(&res.Attachments[i], t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event(t, "equip %s on %s (%s)", best.name, c.StoreItems[best.item].Name, formatCredits(best.cost))
|
|
||||||
case "advisor":
|
case "advisor":
|
||||||
if best.seat > 0 {
|
if best.seat > 0 {
|
||||||
res.Seats++
|
res.Seats++
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
-- Drill upgrade tracks (tracks.go) replace the upgrade tiers and the attachments. Every step
|
||||||
|
-- checks what is there first, so running it again (or on a fresh database) does nothing.
|
||||||
|
-- - Each tool gets a level on its yield and its cycle track. A tool's old tier N becomes
|
||||||
|
-- cycle level N (the tiers made cycles faster; every cycle track has 5 levels), yield 0.
|
||||||
|
-- - Attachments are refunded at what they cost (Turbo Core 900, Diamond Bit 950, Cryo
|
||||||
|
-- Cooler 85,000, Seam Scanner 90,000, the store prices when they were removed), spare
|
||||||
|
-- or equipped, logged as refund_attachments; the table goes.
|
||||||
|
-- - upgrade_item (the tiers' prices) goes: the tracks are read from the game config.
|
||||||
|
-- - Stakes keep their cycle to a tenth of a second (cycle levels such as 2.6 s).
|
||||||
|
|
||||||
|
alter table inventory_item add column if not exists yield_level integer not null default 0
|
||||||
|
check (yield_level >= 0);
|
||||||
|
alter table inventory_item add column if not exists cycle_level integer not null default 0
|
||||||
|
check (cycle_level >= 0);
|
||||||
|
|
||||||
|
do $$
|
||||||
|
begin
|
||||||
|
if exists (select 1 from information_schema.columns
|
||||||
|
where table_name = 'inventory_item' and column_name = 'tier') then
|
||||||
|
update inventory_item set cycle_level = least(tier, 5) where tier > 0;
|
||||||
|
alter table inventory_item drop column tier;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if exists (select 1 from information_schema.tables where table_name = 'attachment') then
|
||||||
|
with refund as (
|
||||||
|
select user_id, sum(case item when 'Turbo Core' then 900 when 'Diamond Bit' then 950
|
||||||
|
when 'Cryo Cooler' then 85000 when 'Seam Scanner' then 90000
|
||||||
|
else 0 end) as credits, count(*) as n
|
||||||
|
from attachment group by user_id
|
||||||
|
), paid as (
|
||||||
|
update bank_account set balance = balance + refund.credits
|
||||||
|
from refund where bank_account.user_id = refund.user_id and refund.credits > 0
|
||||||
|
returning bank_account.user_id, refund.credits, refund.n
|
||||||
|
)
|
||||||
|
insert into event_log(user_id, kind, data)
|
||||||
|
select user_id, 'refund_attachments', jsonb_build_object('refund', credits, 'attachments', n) from paid;
|
||||||
|
drop table attachment;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if exists (select 1 from information_schema.columns
|
||||||
|
where table_name = 'staking_event' and column_name = 'duration_in_secs'
|
||||||
|
and data_type <> 'double precision') then
|
||||||
|
alter table staking_event alter column duration_in_secs type float8;
|
||||||
|
end if;
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
|
||||||
|
drop table if exists upgrade_item;
|
||||||
+6
-25
@@ -28,16 +28,6 @@ create table store_item(
|
|||||||
completion_time_in_secs integer not null
|
completion_time_in_secs integer not null
|
||||||
);
|
);
|
||||||
|
|
||||||
create table upgrade_item(
|
|
||||||
tier integer not null,
|
|
||||||
store_item_id integer not null,
|
|
||||||
price float8 not null,
|
|
||||||
claim_boost float8 not null,
|
|
||||||
constraint fk_store_item_upgrade_item foreign key(store_item_id)
|
|
||||||
references store_item(id) on delete cascade,
|
|
||||||
primary key (store_item_id, tier)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table staking_source (
|
create table staking_source (
|
||||||
id uuid primary key default gen_random_uuid(),
|
id uuid primary key default gen_random_uuid(),
|
||||||
name varchar not null,
|
name varchar not null,
|
||||||
@@ -66,9 +56,13 @@ create table resource_well (
|
|||||||
create table inventory_item (
|
create table inventory_item (
|
||||||
id uuid primary key default gen_random_uuid(),
|
id uuid primary key default gen_random_uuid(),
|
||||||
user_id uuid not null,
|
user_id uuid not null,
|
||||||
tier integer not null default 0,
|
|
||||||
store_item_id int not null,
|
store_item_id int not null,
|
||||||
created_at timestamp with time zone default now(),
|
created_at timestamp with time zone default now(),
|
||||||
|
-- Levels on the tool's upgrade tracks (tracks.go; the values are in the game config).
|
||||||
|
yield_level integer not null default 0
|
||||||
|
check (yield_level >= 0),
|
||||||
|
cycle_level integer not null default 0
|
||||||
|
check (cycle_level >= 0),
|
||||||
constraint fk_user_inventory_item foreign key(user_id)
|
constraint fk_user_inventory_item foreign key(user_id)
|
||||||
references users(id) on delete cascade,
|
references users(id) on delete cascade,
|
||||||
constraint fk_store_item_inventory_item foreign key(store_item_id)
|
constraint fk_store_item_inventory_item foreign key(store_item_id)
|
||||||
@@ -88,7 +82,7 @@ create table staking_event (
|
|||||||
user_id uuid not null,
|
user_id uuid not null,
|
||||||
well_id uuid not null,
|
well_id uuid not null,
|
||||||
inventory_item_id uuid not null,
|
inventory_item_id uuid not null,
|
||||||
duration_in_secs integer not null,
|
duration_in_secs float8 not null, -- to a tenth of a second
|
||||||
stake_amount float8 not null,
|
stake_amount float8 not null,
|
||||||
created_at timestamp with time zone default now(),
|
created_at timestamp with time zone default now(),
|
||||||
-- Wells of the moon it mines (StoreItem.Reach), fixed at stake time; well_id is the
|
-- Wells of the moon it mines (StoreItem.Reach), fixed at stake time; well_id is the
|
||||||
@@ -227,19 +221,6 @@ create table market_pressure (
|
|||||||
primary key (user_id, item)
|
primary key (user_id, item)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Drill attachments (attachments.go) a player owns, by config name; inventory_item_id is
|
|
||||||
-- the tool it's equipped on (null: in the inventory). At most one of each kind per tool.
|
|
||||||
create table attachment (
|
|
||||||
id uuid primary key default gen_random_uuid(),
|
|
||||||
user_id uuid not null references users(id) on delete cascade,
|
|
||||||
item text not null,
|
|
||||||
inventory_item_id uuid references inventory_item(id) on delete set null,
|
|
||||||
created_at timestamp with time zone not null default now()
|
|
||||||
);
|
|
||||||
create index attachment_user on attachment (user_id);
|
|
||||||
create unique index attachment_once on attachment (inventory_item_id, item)
|
|
||||||
where inventory_item_id is not null;
|
|
||||||
|
|
||||||
-- Advisors hired (advisors.go), by config name. Seated ones (seat 1..users.advisor_seats)
|
-- Advisors hired (advisors.go), by config name. Seated ones (seat 1..users.advisor_seats)
|
||||||
-- give their bonus; seat null: hired but not seated.
|
-- give their bonus; seat null: hired but not seated.
|
||||||
create table advisor_hire (
|
create table advisor_hire (
|
||||||
|
|||||||
+15
-12
@@ -75,7 +75,7 @@ func TestTemplates(t *testing.T) {
|
|||||||
{ID: 2, Kind: "admin_grant", Data: "{}", At: now}}, Page: 1, More: true}
|
{ID: 2, Kind: "admin_grant", Data: "{}", At: now}}, Page: 1, More: true}
|
||||||
stake := s.Stakes[0]
|
stake := s.Stakes[0]
|
||||||
playerPage := &PlayerPage{Allowed: true, Self: true, P: player, State: s, Running: 1, Events: events,
|
playerPage := &PlayerPage{Allowed: true, Self: true, P: player, State: s, Running: 1, Events: events,
|
||||||
Tools: []PlayerTool{{Tool: s.Tools[0], Item: &s.Config.StoreItems[0], MaxTier: 3, Stake: &stake,
|
Tools: []PlayerTool{{Tool: s.Tools[0], Item: &s.Config.StoreItems[0], Max: Levels{4, 5}, Stake: &stake,
|
||||||
MoonName: "Moon", Remaining: time.Minute}, {Tool: s.Tools[1], Item: &s.Config.StoreItems[1]}},
|
MoonName: "Moon", Remaining: time.Minute}, {Tool: s.Tools[1], Item: &s.Config.StoreItems[1]}},
|
||||||
StoreItems: s.Config.StoreItems, Grantable: []string{"Sollux"}, Flash: "Done", FlashError: true}
|
StoreItems: s.Config.StoreItems, Grantable: []string{"Sollux"}, Flash: "Done", FlashError: true}
|
||||||
playersPage := &PlayersPage{Allowed: true, Query: "x", Sort: "seen", Players: []PlayerRow{player}, Total: 1}
|
playersPage := &PlayersPage{Allowed: true, Query: "x", Sort: "seen", Players: []PlayerRow{player}, Total: 1}
|
||||||
@@ -198,8 +198,7 @@ func testID(prefix string, n int) string {
|
|||||||
func newPlayerState() *State {
|
func newPlayerState() *State {
|
||||||
c := cfg()
|
c := cfg()
|
||||||
s := &State{UserID: testID("u", 1), Config: Config{StoreItems: c.StoreItems}, Now: time.Now(),
|
s := &State{UserID: testID("u", 1), Config: Config{StoreItems: c.StoreItems}, Now: time.Now(),
|
||||||
Balance: c.StartingCredits, LastWell: map[string]string{}, Fitted: map[string][]string{},
|
Balance: c.StartingCredits, LastWell: map[string]string{}}
|
||||||
Spare: map[string]int{}}
|
|
||||||
for _, name := range sortedKeys(c.ResourcePrices) {
|
for _, name := range sortedKeys(c.ResourcePrices) {
|
||||||
s.Config.Resources = append(s.Config.Resources, name)
|
s.Config.Resources = append(s.Config.Resources, name)
|
||||||
s.Resources = append(s.Resources, ResourceBalance{Resource: name})
|
s.Resources = append(s.Resources, ResourceBalance{Resource: name})
|
||||||
@@ -211,7 +210,7 @@ func newPlayerState() *State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// veteranState has everything unlocked and something of each kind: moons (one mined out),
|
// veteranState has everything unlocked and something of each kind: moons (one mined out),
|
||||||
// drills ready, mining and multi-well, idle tools, attachments, advisors, refinery batches,
|
// drills ready, mining and multi-well, idle and upgraded tools, advisors, refinery batches,
|
||||||
// live boosts and selling pressure.
|
// live boosts and selling pressure.
|
||||||
func veteranState() *State {
|
func veteranState() *State {
|
||||||
c := cfg()
|
c := cfg()
|
||||||
@@ -239,7 +238,15 @@ func veteranState() *State {
|
|||||||
for i := range c.StoreItems {
|
for i := range c.StoreItems {
|
||||||
item := &c.StoreItems[i]
|
item := &c.StoreItems[i]
|
||||||
for n := range 2 {
|
for n := range 2 {
|
||||||
s.Tools = append(s.Tools, Tool{ID: testID("t", i*10+n), StoreItemID: item.ID, Tier: min(n, len(item.Upgrades))})
|
// The second copy upgraded: one level on each track, the top on yield for the first item.
|
||||||
|
tool := Tool{ID: testID("t", i*10+n), StoreItemID: item.ID}
|
||||||
|
if n == 1 {
|
||||||
|
tool.YieldLevel, tool.CycleLevel = min(1, len(item.Tracks.Yield.Levels)), min(1, len(item.Tracks.Cycle.Levels))
|
||||||
|
if i == 0 {
|
||||||
|
tool.YieldLevel = len(item.Tracks.Yield.Levels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Tools = append(s.Tools, tool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ready, mining and multi-well stakes; the second copy of each tool stays idle.
|
// Ready, mining and multi-well stakes; the second copy of each tool stays idle.
|
||||||
@@ -254,8 +261,8 @@ func veteranState() *State {
|
|||||||
start = now.Add(-time.Hour)
|
start = now.Add(-time.Hour)
|
||||||
}
|
}
|
||||||
s.Stakes = append(s.Stakes, Stake{ID: testID("s", i), MoonID: s.Moons[0].ID, WellID: w.ID, ToolID: tool.ID,
|
s.Stakes = append(s.Stakes, Stake{ID: testID("s", i), MoonID: s.Moons[0].ID, WellID: w.ID, ToolID: tool.ID,
|
||||||
Resource: w.Resource, Amount: item.ToolYield(tool.Tier), Start: start,
|
Resource: w.Resource, Amount: item.ToolYield(tool.Levels()), Start: start,
|
||||||
DurationInSecs: item.CompletionTimeInSecs, Reach: item.ToolReach(tool.Tier)})
|
DurationInSecs: item.ToolSecs(tool.Levels()), Reach: item.ToolReach()})
|
||||||
}
|
}
|
||||||
// A crowded, over-drilled well: more drills than the wells table draws timer bars for,
|
// A crowded, over-drilled well: more drills than the wells table draws timer bars for,
|
||||||
// the later ones short or getting nothing.
|
// the later ones short or getting nothing.
|
||||||
@@ -265,7 +272,7 @@ func veteranState() *State {
|
|||||||
s.Tools = append(s.Tools, tool)
|
s.Tools = append(s.Tools, tool)
|
||||||
s.Stakes = append(s.Stakes, Stake{ID: testID("s", 900+n), MoonID: s.Moons[0].ID, WellID: wells[1].ID,
|
s.Stakes = append(s.Stakes, Stake{ID: testID("s", 900+n), MoonID: s.Moons[0].ID, WellID: wells[1].ID,
|
||||||
ToolID: tool.ID, Resource: wells[1].Resource, Amount: 200, Start: now.Add(time.Duration(n) * time.Second),
|
ToolID: tool.ID, Resource: wells[1].Resource, Amount: 200, Start: now.Add(time.Duration(n) * time.Second),
|
||||||
DurationInSecs: item.CompletionTimeInSecs, Reach: 1})
|
DurationInSecs: float64(item.CompletionTimeInSecs), Reach: 1})
|
||||||
}
|
}
|
||||||
s.LastWell[s.Tools[1].ID] = wells[1].ID
|
s.LastWell[s.Tools[1].ID] = wells[1].ID
|
||||||
s.RefinerySlots = 2
|
s.RefinerySlots = 2
|
||||||
@@ -281,10 +288,6 @@ func veteranState() *State {
|
|||||||
s.CycleBoosts = PriceBoosts{"all": 2}
|
s.CycleBoosts = PriceBoosts{"all": 2}
|
||||||
s.YieldBoosts = PriceBoosts{first: 2}
|
s.YieldBoosts = PriceBoosts{first: 2}
|
||||||
s.Pressure = map[string]float64{first: 50}
|
s.Pressure = map[string]float64{first: 50}
|
||||||
if len(c.Attachments.Items) > 1 {
|
|
||||||
s.Fitted[s.Tools[0].ID] = []string{c.Attachments.Items[0].Name}
|
|
||||||
s.Spare[c.Attachments.Items[1].Name] = 2
|
|
||||||
}
|
|
||||||
// Advisors: one seated, one hired but not seated, a free seat.
|
// Advisors: one seated, one hired but not seated, a free seat.
|
||||||
if len(c.Advisors.Items) > 1 {
|
if len(c.Advisors.Items) > 1 {
|
||||||
s.Hires = []AdvisorHire{{Name: c.Advisors.Items[0].Name, Seat: 1}, {Name: c.Advisors.Items[1].Name}}
|
s.Hires = []AdvisorHire{{Name: c.Advisors.Items[0].Name, Seat: 1}, {Name: c.Advisors.Items[1].Name}}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Content-Type: application/x-www-form-urlencoded
|
|||||||
userId=:user_id
|
userId=:user_id
|
||||||
|
|
||||||
# A view: s=moons|equipment|market|store|refinery|advisors|leaderboard|help|new-moon, moon=<id>, well=<id>,
|
# A view: s=moons|equipment|market|store|refinery|advisors|leaderboard|help|new-moon, moon=<id>, well=<id>,
|
||||||
# drill=<tool id> (equipment: the tool's attachment panel),
|
# drill=<tool id> (equipment: the tool's panel, its upgrade tracks),
|
||||||
# pick=<resource or refined item> (market: the selected quote), list=past (moons: the Past tab,
|
# pick=<resource or refined item> (market: the selected quote), list=past (moons: the Past tab,
|
||||||
# the only way a mined-out moon shows), page=<n> (with list=past: 10 past moons a page, 1 the
|
# the only way a mined-out moon shows), page=<n> (with list=past: 10 past moons a page, 1 the
|
||||||
# most recent), adv=<advisor name> (advisors: a hired advisor's panel)
|
# most recent), adv=<advisor name> (advisors: a hired advisor's panel)
|
||||||
@@ -45,25 +45,20 @@ POST :host/act/sell-moon
|
|||||||
:headers
|
:headers
|
||||||
sell-moon=<moon id>
|
sell-moon=<moon id>
|
||||||
|
|
||||||
# Sell an idle tool (not the only one); its attachments go back to the inventory
|
# Sell an idle tool (not the only one)
|
||||||
POST :host/act/sell-tool
|
POST :host/act/sell-tool
|
||||||
:headers
|
:headers
|
||||||
tool=<tool id>
|
tool=<tool id>
|
||||||
|
|
||||||
# Sell one spare (unequipped) attachment
|
|
||||||
POST :host/act/sell-attachment
|
|
||||||
:headers
|
|
||||||
item=Turbo Core
|
|
||||||
|
|
||||||
# Buy a tool
|
# Buy a tool
|
||||||
POST :host/act/buy-tool
|
POST :host/act/buy-tool
|
||||||
:headers
|
:headers
|
||||||
id=1
|
id=1
|
||||||
|
|
||||||
# Upgrade a tool
|
# Upgrade a tool: one level on its yield or cycle track (track=yield|cycle)
|
||||||
POST :host/act/upgrade
|
POST :host/act/upgrade
|
||||||
:headers
|
:headers
|
||||||
tool=2bd5f846-ce1d-45be-998e-26d2e97b7f68
|
tool=2bd5f846-ce1d-45be-998e-26d2e97b7f68&track=yield
|
||||||
|
|
||||||
# Deploy a tool on a well
|
# Deploy a tool on a well
|
||||||
POST :host/act/stake
|
POST :host/act/stake
|
||||||
@@ -114,20 +109,6 @@ POST :host/act/buy-slot
|
|||||||
POST :host/act/upgrade-batch
|
POST :host/act/upgrade-batch
|
||||||
:headers
|
:headers
|
||||||
|
|
||||||
# Buy a drill attachment (item = config name) into the inventory
|
|
||||||
POST :host/act/buy-attachment
|
|
||||||
:headers
|
|
||||||
item=Turbo+Core
|
|
||||||
|
|
||||||
# Equip a spare attachment on a tool (to), or unequip it. Counts from the tool's next stake.
|
|
||||||
POST :host/act/equip
|
|
||||||
:headers
|
|
||||||
item=Turbo+Core&to=2bd5f846-ce1d-45be-998e-26d2e97b7f68
|
|
||||||
|
|
||||||
POST :host/act/unequip
|
|
||||||
:headers
|
|
||||||
item=Turbo+Core&to=2bd5f846-ce1d-45be-998e-26d2e97b7f68
|
|
||||||
|
|
||||||
# Hire an advisor (config name) into a free seat
|
# Hire an advisor (config name) into a free seat
|
||||||
POST :host/act/hire
|
POST :host/act/hire
|
||||||
:headers
|
:headers
|
||||||
@@ -234,9 +215,8 @@ Content-Type: application/json
|
|||||||
|
|
||||||
{"catalog": 0, "clientSeq": 2}
|
{"catalog": 0, "clientSeq": 2}
|
||||||
|
|
||||||
# Other sections' actions (clientSeq is echoed back): buy-tool (id), upgrade (tool),
|
# Other sections' actions (clientSeq is echoed back): buy-tool (id), upgrade (tool, track),
|
||||||
# sell-tool (tool), buy-attachment (item), equip/unequip (item, to), sell-attachment (item),
|
# sell-tool (tool), refine (recipe, qty or all), collect (job or none), buy-slot, upgrade-batch, hire (advisor), buy-seat,
|
||||||
# refine (recipe, qty or all), collect (job or none), buy-slot, upgrade-batch, hire (advisor), buy-seat,
|
|
||||||
# seat-advisor (advisor, seat), unseat-advisor (advisor), swap-advisors (advisor, with),
|
# seat-advisor (advisor, seat), unseat-advisor (advisor), swap-advisors (advisor, with),
|
||||||
# dismiss-advisor (advisor), sell-moon (sell-moon)
|
# dismiss-advisor (advisor), sell-moon (sell-moon)
|
||||||
POST :host/api/act/buy-tool
|
POST :host/api/act/buy-tool
|
||||||
@@ -332,10 +312,10 @@ POST :host/utils/players/30f76186-83ff-4b26-a429-357bc1ee126f/resources
|
|||||||
:headers
|
:headers
|
||||||
resource=Sollux&amount=500
|
resource=Sollux&amount=500
|
||||||
|
|
||||||
# Admin: give a tool (store item id, tier 0..number of upgrades)
|
# Admin: give a tool (store item id, yield and cycle track levels from 0)
|
||||||
POST :host/utils/players/30f76186-83ff-4b26-a429-357bc1ee126f/tool
|
POST :host/utils/players/30f76186-83ff-4b26-a429-357bc1ee126f/tool
|
||||||
:headers
|
:headers
|
||||||
item=1&tier=2
|
item=1&yield=2&cycle=1
|
||||||
|
|
||||||
# Admin: finish all the player's running drills now (they can claim right away)
|
# Admin: finish all the player's running drills now (they can claim right away)
|
||||||
POST :host/utils/players/30f76186-83ff-4b26-a429-357bc1ee126f/finish-stakes
|
POST :host/utils/players/30f76186-83ff-4b26-a429-357bc1ee126f/finish-stakes
|
||||||
|
|||||||
@@ -0,0 +1,426 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Drill upgrade tracks. Every store item has two named tracks of its own (config
|
||||||
|
// storeItems[].tracks): yield (the claim amount per cycle) and cycle (seconds per cycle).
|
||||||
|
// Each level of a track sets the value flat (not a multiplier) and has a price and,
|
||||||
|
// higher up, refined parts (refinery.go). A tool's reach is the item's and never changes.
|
||||||
|
// Levels belong to one tool (inventory_item.yield_level, cycle_level): copies upgrade
|
||||||
|
// separately, and copies with the same levels are interchangeable (pickTool).
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TrackLevel is one level of a track: the track's value from this level on (the claim
|
||||||
|
// amount, or the cycle in seconds), its price in credits and the refined items it uses up.
|
||||||
|
type TrackLevel struct {
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Parts map[string]float64 `json:"parts,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track is one of a drill's upgrade tracks; level 0 is the item's own value.
|
||||||
|
type Track struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Levels []TrackLevel `json:"levels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracks are a drill's two upgrade tracks.
|
||||||
|
type Tracks struct {
|
||||||
|
Yield Track `json:"yield"`
|
||||||
|
Cycle Track `json:"cycle"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// trackKinds are the tracks, in the order the views show them.
|
||||||
|
var trackKinds = []string{"yield", "cycle"}
|
||||||
|
|
||||||
|
// Levels are a tool's level on each track (0: the item's base values).
|
||||||
|
type Levels struct {
|
||||||
|
Yield, Cycle int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Tool) Levels() Levels { return Levels{t.YieldLevel, t.CycleLevel} }
|
||||||
|
|
||||||
|
// Of is the level on the track of the kind.
|
||||||
|
func (lv Levels) Of(kind string) int {
|
||||||
|
if kind == "cycle" {
|
||||||
|
return lv.Cycle
|
||||||
|
}
|
||||||
|
return lv.Yield
|
||||||
|
}
|
||||||
|
|
||||||
|
// With is lv with the track of the kind at level n.
|
||||||
|
func (lv Levels) With(kind string, n int) Levels {
|
||||||
|
if kind == "cycle" {
|
||||||
|
lv.Cycle = n
|
||||||
|
} else {
|
||||||
|
lv.Yield = n
|
||||||
|
}
|
||||||
|
return lv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track is the item's track of the kind (yield or cycle), nil for another kind.
|
||||||
|
func (item *StoreItem) Track(kind string) *Track {
|
||||||
|
switch kind {
|
||||||
|
case "yield":
|
||||||
|
return &item.Tracks.Yield
|
||||||
|
case "cycle":
|
||||||
|
return &item.Tracks.Cycle
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackLevel is level n (1 on) of the track of the kind, nil past its top.
|
||||||
|
func (item *StoreItem) TrackLevel(kind string, n int) *TrackLevel {
|
||||||
|
if t := item.Track(kind); t != nil && n >= 1 && n <= len(t.Levels) {
|
||||||
|
return &t.Levels[n-1]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// trackValue is the track's value at level n: the base below level 1, the top past it.
|
||||||
|
func (t *Track) value(base float64, n int) float64 {
|
||||||
|
if n <= 0 || len(t.Levels) == 0 {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
return t.Levels[min(n, len(t.Levels))-1].Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolYield is what one completed stake returns at the levels, before bonuses.
|
||||||
|
func (item *StoreItem) ToolYield(lv Levels) float64 {
|
||||||
|
return item.Tracks.Yield.value(item.ClaimAmount, lv.Yield)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolSecs is a stake's cycle at the levels, before bonuses.
|
||||||
|
func (item *StoreItem) ToolSecs(lv Levels) float64 {
|
||||||
|
return item.Tracks.Cycle.value(float64(item.CompletionTimeInSecs), lv.Cycle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToolReach is how many wells a stake mines: the item's reach, at least 1.
|
||||||
|
func (item *StoreItem) ToolReach() int { return max(1, item.Reach) }
|
||||||
|
|
||||||
|
// MaxLevels is the top level of each track.
|
||||||
|
func (item *StoreItem) MaxLevels() Levels {
|
||||||
|
return Levels{len(item.Tracks.Yield.Levels), len(item.Tracks.Cycle.Levels)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// trackParts is what level n of the item's track uses up, from the config.
|
||||||
|
func trackParts(storeItemID int, kind string, n int) map[string]float64 {
|
||||||
|
if item := configItem(storeItemID); item != nil {
|
||||||
|
if l := item.TrackLevel(kind, n); l != nil {
|
||||||
|
return l.Parts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgradesPrice is the credits paid for the levels: what a tool's upgrades cost.
|
||||||
|
func (item *StoreItem) upgradesPrice(lv Levels) float64 {
|
||||||
|
sum := 0.0
|
||||||
|
for _, kind := range trackKinds {
|
||||||
|
for n := 1; n <= lv.Of(kind); n++ {
|
||||||
|
if l := item.TrackLevel(kind, n); l != nil {
|
||||||
|
sum += l.Price
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GameConfig) validateTracks(item StoreItem) error {
|
||||||
|
for _, kind := range trackKinds {
|
||||||
|
t := item.Track(kind)
|
||||||
|
if len(t.Levels) > 0 && t.Name == "" {
|
||||||
|
return fmt.Errorf("store item %d: the %s track needs a name", item.ID, kind)
|
||||||
|
}
|
||||||
|
prev := item.ToolYield(Levels{})
|
||||||
|
if kind == "cycle" {
|
||||||
|
prev = item.ToolSecs(Levels{})
|
||||||
|
}
|
||||||
|
for i, l := range t.Levels {
|
||||||
|
better := l.Value > prev
|
||||||
|
if kind == "cycle" {
|
||||||
|
better = l.Value < prev && l.Value >= 1
|
||||||
|
}
|
||||||
|
if !better || l.Price < 0 {
|
||||||
|
return fmt.Errorf("store item %d %s level %d: each level must improve on the one before (cycles at least 1 s), the price can't be negative",
|
||||||
|
item.ID, kind, i+1)
|
||||||
|
}
|
||||||
|
prev = l.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ rule
|
||||||
|
|
||||||
|
// upgradeTool raises a tool's track of the kind (yield or cycle) a level (an idle copy of
|
||||||
|
// it, if there is one: see pickTool) and returns the tool it upgraded.
|
||||||
|
func upgradeTool(ctx context.Context, db *pgxpool.Pool, userID, toolID, kind string) (string, error) {
|
||||||
|
if kind != "yield" && kind != "cycle" {
|
||||||
|
return "", gameErr("Pick an upgrade track")
|
||||||
|
}
|
||||||
|
err := inTx(ctx, db, func(tx pgx.Tx) error {
|
||||||
|
var err error
|
||||||
|
if toolID, err = pickTool(ctx, tx, userID, toolID, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var storeItemID int
|
||||||
|
var lv Levels
|
||||||
|
err = tx.QueryRow(ctx, "select store_item_id, yield_level, cycle_level from inventory_item where id = $1",
|
||||||
|
toolID).Scan(&storeItemID, &lv.Yield, &lv.Cycle)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
item := configItem(storeItemID)
|
||||||
|
if item == nil {
|
||||||
|
return gameErr("That tool can't be upgraded")
|
||||||
|
}
|
||||||
|
n := lv.Of(kind) + 1
|
||||||
|
next := item.TrackLevel(kind, n)
|
||||||
|
if next == nil {
|
||||||
|
return gameErr("%s is already at its top level", item.Track(kind).Name)
|
||||||
|
}
|
||||||
|
if err := debit(ctx, tx, userID, next.Price); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := takeItems(ctx, tx, userID, next.Parts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(ctx, "update inventory_item set "+kind+"_level = $2 where id = $1", toolID, n)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(ctx, "insert into upgrade_event(inventory_item_id) values ($1)", toolID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return logEvent(ctx, tx, userID, "upgrade",
|
||||||
|
map[string]any{"item": storeItemID, "track": kind, "level": n, "price": next.Price})
|
||||||
|
})
|
||||||
|
return toolID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// roundSecs rounds a cycle to a tenth of a second.
|
||||||
|
func roundSecs(s float64) float64 { return math.Round(s*10) / 10 }
|
||||||
|
|
||||||
|
// lockTool locks one of the player's tools.
|
||||||
|
func lockTool(ctx context.Context, tx pgx.Tx, userID, toolID string) error {
|
||||||
|
if !uuidPattern.MatchString(toolID) {
|
||||||
|
return gameErr("Tool not found")
|
||||||
|
}
|
||||||
|
var id string
|
||||||
|
err := tx.QueryRow(ctx, `
|
||||||
|
select id from inventory_item where id = $1 and user_id = $2 for update`, toolID, userID).Scan(&id)
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return gameErr("Tool not found")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ view
|
||||||
|
|
||||||
|
// Tag is the levels as a short tag after a tool's name (" Y2 C1"), "" for a base tool.
|
||||||
|
func (lv Levels) Tag() string {
|
||||||
|
if lv.Yield == 0 && lv.Cycle == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(" Y%d C%d", lv.Yield, lv.Cycle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title says the levels in words, for tooltips.
|
||||||
|
func (lv Levels) Title() string {
|
||||||
|
return fmt.Sprintf("Yield level %d, cycle level %d", lv.Yield, lv.Cycle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackRow is one of a tool's upgrade tracks (a ToolRow's), for the Equipment card and the
|
||||||
|
// drill panel. Values are a stake's now, with the player's bonuses and live events: the
|
||||||
|
// yield per cycle, or the cycle in seconds.
|
||||||
|
type TrackRow struct {
|
||||||
|
Kind string // yield, cycle
|
||||||
|
Name string // the track's name for this drill, e.g. "Twin rotors"
|
||||||
|
Level int
|
||||||
|
Max int
|
||||||
|
Value float64
|
||||||
|
Next *TrackLevel // nil at the top level
|
||||||
|
NextValue float64
|
||||||
|
CanAfford bool // credits and parts
|
||||||
|
Parts []PartRow // refined items the next level uses (refinery.go)
|
||||||
|
Missing string // why it can't be bought, if it can't
|
||||||
|
Short string // "Need ₥X more" when credits are short for it
|
||||||
|
RateFrom float64 // credits per hour (creditsPerHour) now and after the next level
|
||||||
|
RateTo float64
|
||||||
|
Cost float64 // the next level's price plus its parts at sell value
|
||||||
|
Ladder []TrackStep // every level, the other track as it is
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackStep is one level of a track, for the drill panel's ladder.
|
||||||
|
type TrackStep struct {
|
||||||
|
Level int
|
||||||
|
Value float64 // with the player's bonuses
|
||||||
|
Rate float64 // credits per hour at this level
|
||||||
|
Price float64 // 0 for level 0
|
||||||
|
Parts []PartRow
|
||||||
|
Current, Next bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle is a cycle track.
|
||||||
|
func (r TrackRow) Cycle() bool { return r.Kind == "cycle" }
|
||||||
|
|
||||||
|
// RateGain is what the next level adds per hour.
|
||||||
|
func (r TrackRow) RateGain() float64 { return r.RateTo - r.RateFrom }
|
||||||
|
|
||||||
|
// Payback is how long the next level's extra income takes to repay its cost, "" if never.
|
||||||
|
func (r TrackRow) Payback() string {
|
||||||
|
if gain := r.RateGain(); gain > 0 {
|
||||||
|
return formatSpan(r.Cost / gain * 3600)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label is the button's text: "Twin rotors 2" (the level it buys).
|
||||||
|
func (r TrackRow) Label() string { return fmt.Sprintf("%s %d", r.Name, r.Level+1) }
|
||||||
|
|
||||||
|
// trackValue is a stake's yield or cycle (by kind) of the item at the levels, now.
|
||||||
|
func (s *State) trackValue(item *StoreItem, lv Levels, kind string) (value, rate float64) {
|
||||||
|
secs, yield := s.drillAt(item, lv, "")
|
||||||
|
rate = s.creditsPerHour(yield, secs, item.ToolReach())
|
||||||
|
if kind == "cycle" {
|
||||||
|
return secs, rate
|
||||||
|
}
|
||||||
|
return yield, rate
|
||||||
|
}
|
||||||
|
|
||||||
|
// trackRows are the tool's two tracks with their next levels and ladders.
|
||||||
|
func (s *State) trackRows(t Tool, item *StoreItem) []TrackRow {
|
||||||
|
var rows []TrackRow
|
||||||
|
lv := t.Levels()
|
||||||
|
for _, kind := range trackKinds {
|
||||||
|
tr := item.Track(kind)
|
||||||
|
if len(tr.Levels) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r := TrackRow{Kind: kind, Name: tr.Name, Level: lv.Of(kind), Max: len(tr.Levels)}
|
||||||
|
r.Value, r.RateFrom = s.trackValue(item, lv, kind)
|
||||||
|
for n := 0; n <= r.Max; n++ {
|
||||||
|
st := TrackStep{Level: n, Current: n == r.Level, Next: n == r.Level+1}
|
||||||
|
st.Value, st.Rate = s.trackValue(item, lv.With(kind, n), kind)
|
||||||
|
if l := item.TrackLevel(kind, n); l != nil {
|
||||||
|
st.Price, st.Parts = l.Price, s.partRows(l.Parts)
|
||||||
|
}
|
||||||
|
r.Ladder = append(r.Ladder, st)
|
||||||
|
}
|
||||||
|
if next := item.TrackLevel(kind, r.Level+1); next != nil {
|
||||||
|
r.Next = next
|
||||||
|
r.NextValue, r.RateTo = s.trackValue(item, lv.With(kind, r.Level+1), kind)
|
||||||
|
r.Cost = next.Price + partsValue(cfg(), next.Parts)
|
||||||
|
r.Parts = s.partRows(next.Parts)
|
||||||
|
r.CanAfford = s.Balance >= next.Price
|
||||||
|
if !r.CanAfford {
|
||||||
|
r.Missing = shortBy(next.Price - s.Balance)
|
||||||
|
r.Short = r.Missing
|
||||||
|
}
|
||||||
|
for _, part := range r.Parts {
|
||||||
|
if !part.OK() && r.CanAfford {
|
||||||
|
r.CanAfford = false
|
||||||
|
r.Missing = fmt.Sprintf("Need %s %s (you have %s). Make it in the Refinery",
|
||||||
|
formatMoney(part.Need), part.Name, formatMoney(part.Have))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows = append(rows, r)
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgradedText names what a tool's latest upgrade on the track gave, for the toast.
|
||||||
|
func (s *State) upgradedText(t Tool, kind string) string {
|
||||||
|
item := s.StoreItem(t.StoreItemID)
|
||||||
|
lv := t.Levels()
|
||||||
|
if item == nil || item.Track(kind) == nil || lv.Of(kind) < 1 {
|
||||||
|
return "Tool upgraded"
|
||||||
|
}
|
||||||
|
before, _ := s.trackValue(item, lv.With(kind, lv.Of(kind)-1), kind)
|
||||||
|
now, _ := s.trackValue(item, lv, kind)
|
||||||
|
name := fmt.Sprintf("%s %d", item.Track(kind).Name, lv.Of(kind))
|
||||||
|
if kind == "cycle" {
|
||||||
|
return fmt.Sprintf("%s: a cycle takes %s (was %s)", name, formatCycle(now), formatCycle(before))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s: +%s per cycle (was +%s)", name, formatMoney(now), formatMoney(before))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrillPanel is the side panel for a drill selected in the Equipment section: its stats,
|
||||||
|
// both upgrade tracks with their ladders, deploying an idle copy and selling it.
|
||||||
|
type DrillPanel struct {
|
||||||
|
ToolRow
|
||||||
|
Mining bool
|
||||||
|
Resale float64 // what it sells for (resale.go)
|
||||||
|
NoSale string // why it can't be sold, if it can't
|
||||||
|
|
||||||
|
// While it mines: the idle tools of its type, the best one and its best well.
|
||||||
|
IdleSame int
|
||||||
|
IdleBest *Tool
|
||||||
|
DeployTo *Suggestion
|
||||||
|
IdleURL string // opens IdleBest's panel
|
||||||
|
|
||||||
|
// Store: the Store is unlocked, so the panel offers upgrades and selling (actions gated
|
||||||
|
// on "store", onboarding.go). Before that it shows stats and the ladders only.
|
||||||
|
Store bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// noSale is why a tool can't be sold (only-tool, claim-first, mining), "" if it can.
|
||||||
|
func (s *State) noSale(row ToolRow) string {
|
||||||
|
switch {
|
||||||
|
case len(s.Tools) <= 1:
|
||||||
|
return "only-tool"
|
||||||
|
case row.Stake != nil && row.Stake.Ready:
|
||||||
|
return "claim-first"
|
||||||
|
case row.Stake != nil:
|
||||||
|
return "mining"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// SellConfirm is the question before selling the drill (its Sell button's confirm dialog).
|
||||||
|
func (d *DrillPanel) SellConfirm() string {
|
||||||
|
return fmt.Sprintf("Sell this %s%s for %s?", d.Item.Name, d.Levels().Tag(), formatCredits(d.Resale))
|
||||||
|
}
|
||||||
|
|
||||||
|
var noSaleText = map[string]string{
|
||||||
|
"only-tool": "You can't sell your only tool",
|
||||||
|
"claim-first": "Claim its haul first",
|
||||||
|
"mining": "Still mining. Sell it once it's idle",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) drillPanel(row ToolRow) *DrillPanel {
|
||||||
|
d := &DrillPanel{ToolRow: row, Mining: row.Stake != nil, Resale: row.Item.ToolResale(row.Levels()),
|
||||||
|
Store: s.Unlocked("store")}
|
||||||
|
d.NoSale = noSaleText[s.noSale(row)]
|
||||||
|
if d.Mining {
|
||||||
|
// The most levels: the one pickTool would deploy.
|
||||||
|
for i := range s.Tools {
|
||||||
|
t := &s.Tools[i]
|
||||||
|
if t.StoreItemID != row.StoreItemID || s.StakeForTool(t.ID) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
d.IdleSame++
|
||||||
|
if b := d.IdleBest; b == nil || t.YieldLevel+t.CycleLevel > b.YieldLevel+b.CycleLevel ||
|
||||||
|
(t.YieldLevel+t.CycleLevel == b.YieldLevel+b.CycleLevel && t.YieldLevel > b.YieldLevel) {
|
||||||
|
d.IdleBest = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.IdleBest != nil {
|
||||||
|
d.IdleURL = View{Section: "equipment", Drill: d.IdleBest.ID}.URL()
|
||||||
|
if m := s.buySuggestions(d.IdleBest.ID); m != nil && len(m.Suggestions) > 0 {
|
||||||
|
d.DeployTo = &m.Suggestions[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
+53
-156
@@ -38,7 +38,14 @@ var templateFuncs = template.FuncMap{
|
|||||||
// short is the tooltip on a button the player can't afford.
|
// short is the tooltip on a button the player can't afford.
|
||||||
"short": func(balance, price float64) string { return shortBy(price - balance) },
|
"short": func(balance, price float64) string { return shortBy(price - balance) },
|
||||||
"mins": func(m int) string { return formatDuration(time.Duration(m) * time.Minute) },
|
"mins": func(m int) string { return formatDuration(time.Duration(m) * time.Minute) },
|
||||||
"secs": formatCycle,
|
"secs": secsText,
|
||||||
|
// A track's value (tracks.go): a cycle ("2.6s") or a yield per cycle ("+5").
|
||||||
|
"trackval": func(cycle bool, v float64) string {
|
||||||
|
if cycle {
|
||||||
|
return formatCycle(v)
|
||||||
|
}
|
||||||
|
return "+" + formatMoney(v)
|
||||||
|
},
|
||||||
"curk": formatCompact,
|
"curk": formatCompact,
|
||||||
"wells": wellsText,
|
"wells": wellsText,
|
||||||
"dur": formatDuration,
|
"dur": formatDuration,
|
||||||
@@ -125,7 +132,11 @@ func formatDuration(d time.Duration) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// formatCycle formats a drill cycle in seconds: "3s", "45s", "2m", "2m 30s".
|
// formatCycle formats a drill cycle in seconds: "3s", "45s", "2m", "2m 30s".
|
||||||
func formatCycle(secs int) string {
|
func formatCycle(f float64) string {
|
||||||
|
if f < 60 && f != math.Trunc(f) {
|
||||||
|
return strconv.FormatFloat(roundSecs(f), 'f', -1, 64) + "s"
|
||||||
|
}
|
||||||
|
secs := int(math.Round(f))
|
||||||
if secs < 60 {
|
if secs < 60 {
|
||||||
return fmt.Sprintf("%ds", secs)
|
return fmt.Sprintf("%ds", secs)
|
||||||
}
|
}
|
||||||
@@ -206,8 +217,8 @@ func (v View) resolve(s *State) View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A tool's panel opens with Equipment; what in it belongs to the Store (upgrade,
|
// A tool's panel opens with Equipment; what in it belongs to the Store (upgrades,
|
||||||
// attachments, selling) shows once the Store is unlocked (DrillPanel.Store).
|
// selling) shows once the Store is unlocked (DrillPanel.Store).
|
||||||
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 = ""
|
||||||
}
|
}
|
||||||
@@ -318,7 +329,6 @@ type Page struct {
|
|||||||
Trade *MarketPage // market section: quotes, chart, portfolio (market.go)
|
Trade *MarketPage // market section: quotes, chart, portfolio (market.go)
|
||||||
MaxSupply float64 // a full supply bar: the selected moon's grade (else the default grade)
|
MaxSupply float64 // a full supply bar: the selected moon's grade (else the default grade)
|
||||||
Refinery *RefineryPage // refinery section (refinery.go)
|
Refinery *RefineryPage // refinery section (refinery.go)
|
||||||
Gear []AttachmentCard // store section (attachments.go)
|
|
||||||
Drill *DrillPanel // equipment section, with a tool selected
|
Drill *DrillPanel // equipment section, with a tool selected
|
||||||
Advisors *AdvisorsPage // advisors section (advisors.go)
|
Advisors *AdvisorsPage // advisors section (advisors.go)
|
||||||
Ranking *LeaderboardPage // leaderboard section (leaderboard.go), loaded by the server
|
Ranking *LeaderboardPage // leaderboard section (leaderboard.go), loaded by the server
|
||||||
@@ -490,8 +500,7 @@ type WellDetail struct {
|
|||||||
type StakeRow struct {
|
type StakeRow struct {
|
||||||
Stake
|
Stake
|
||||||
Item *StoreItem
|
Item *StoreItem
|
||||||
Tier int
|
Levels Levels // the tool's track levels
|
||||||
Gear []*Attachment
|
|
||||||
Ready bool
|
Ready bool
|
||||||
Remaining time.Duration
|
Remaining time.Duration
|
||||||
Take float64 // what claiming it would get now, from what its wells hold
|
Take float64 // what claiming it would get now, from what its wells hold
|
||||||
@@ -505,14 +514,13 @@ type StakeRow struct {
|
|||||||
// ReachText says how many wells it mines ("" for one).
|
// ReachText says how many wells it mines ("" for one).
|
||||||
func (r StakeRow) ReachText() string { return reachLabel(r.Reach) }
|
func (r StakeRow) ReachText() string { return reachLabel(r.Reach) }
|
||||||
|
|
||||||
// ToolGroup is tools of the same type, tier and attachments. Secs and Yield are what a
|
// ToolGroup is tools of the same type and track levels. Secs and Yield are what a stake
|
||||||
// stake would take and return now, with the player's bonuses and live events.
|
// would take and return now, with the player's bonuses and live events.
|
||||||
type ToolGroup struct {
|
type ToolGroup struct {
|
||||||
Tool
|
Tool
|
||||||
Item *StoreItem
|
Item *StoreItem
|
||||||
Gear []*Attachment
|
|
||||||
Count int
|
Count int
|
||||||
Secs int
|
Secs float64
|
||||||
Yield float64
|
Yield float64
|
||||||
Reach int // wells of a moon one cycle mines
|
Reach int // wells of a moon one cycle mines
|
||||||
Here float64 // what a cycle gets from the selected well's moon after its current drills (the well panel)
|
Here float64 // what a cycle gets from the selected well's moon after its current drills (the well panel)
|
||||||
@@ -550,18 +558,10 @@ func reachLabel(reach int) string {
|
|||||||
|
|
||||||
type ToolRow struct {
|
type ToolRow struct {
|
||||||
ToolGroup
|
ToolGroup
|
||||||
FreeSlots int // attachment slots left
|
|
||||||
Selected bool
|
Selected bool
|
||||||
Idle int // copies (Count) not mining
|
Idle int // copies (Count) not mining
|
||||||
MaxTier int
|
|
||||||
Next *Upgrade
|
|
||||||
CanAfford bool
|
|
||||||
Stake *StakeRow
|
Stake *StakeRow
|
||||||
Parts []PartRow // refined items the next upgrade uses (refinery.go)
|
Tracks []TrackRow // its yield and cycle upgrade tracks (tracks.go)
|
||||||
Missing string // why the upgrade can't be bought, if it can't
|
|
||||||
Short string // "Need ₥X more" when credits are short for it
|
|
||||||
Preview *UpgradePreview
|
|
||||||
Ladder []TierRow // every tier, with the player's bonuses
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type StoreRow struct {
|
type StoreRow struct {
|
||||||
@@ -576,7 +576,7 @@ type StoreRow struct {
|
|||||||
func (r StoreRow) RateGain() float64 { return r.Rate - r.BestRate }
|
func (r StoreRow) RateGain() float64 { return r.Rate - r.BestRate }
|
||||||
|
|
||||||
// ReachText says how many wells the tool mines ("" for one).
|
// ReachText says how many wells the tool mines ("" for one).
|
||||||
func (r StoreRow) ReachText() string { return reachLabel(r.Item.ToolReach(0)) }
|
func (r StoreRow) ReachText() string { return reachLabel(r.Item.ToolReach()) }
|
||||||
|
|
||||||
type MarketRow struct {
|
type MarketRow struct {
|
||||||
ResourceBalance
|
ResourceBalance
|
||||||
@@ -640,7 +640,7 @@ func (s *State) stakeRow(st Stake) StakeRow {
|
|||||||
row := StakeRow{Stake: st, Remaining: st.Remaining(s.Now), Take: st.Amount}
|
row := StakeRow{Stake: st, Remaining: st.Remaining(s.Now), Take: st.Amount}
|
||||||
row.Ready = row.Remaining == 0
|
row.Ready = row.Remaining == 0
|
||||||
if t := s.tool(st.ToolID); t != nil {
|
if t := s.tool(st.ToolID); t != nil {
|
||||||
row.Item, row.Tier, row.Gear = s.StoreItem(t.StoreItemID), t.Tier, s.gear(t.ID)
|
row.Item, row.Levels = s.StoreItem(t.StoreItemID), t.Levels()
|
||||||
}
|
}
|
||||||
if m := s.moon(st.MoonID); m != nil {
|
if m := s.moon(st.MoonID); m != nil {
|
||||||
supply := m.supply()
|
supply := m.supply()
|
||||||
@@ -738,12 +738,12 @@ func (s *State) wideGroup(m *Moon) *DrillGroup {
|
|||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
// toolGroups groups tools by type, tier and attachments (and with all, whether they're
|
// toolGroups groups tools by type and track levels (and with all, whether they're
|
||||||
// mining), so duplicates show as one row. Yield is for a well of the resource ("" for any).
|
// mining), so duplicates show as one row. Yield is for a well of the resource ("" for any).
|
||||||
func (s *State) toolGroups(idleOnly bool, resource string) []ToolGroup {
|
func (s *State) toolGroups(idleOnly bool, resource string) []ToolGroup {
|
||||||
var groups []ToolGroup
|
var groups []ToolGroup
|
||||||
key := func(t Tool) string {
|
key := func(t Tool) string {
|
||||||
return fmt.Sprint(t.StoreItemID, t.Tier, s.Fitted[t.ID], s.StakeForTool(t.ID) != nil)
|
return fmt.Sprint(t.StoreItemID, t.Levels(), s.StakeForTool(t.ID) != nil)
|
||||||
}
|
}
|
||||||
keys := map[string]int{}
|
keys := map[string]int{}
|
||||||
for _, t := range s.Tools {
|
for _, t := range s.Tools {
|
||||||
@@ -759,7 +759,7 @@ func (s *State) toolGroups(idleOnly bool, resource string) []ToolGroup {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
keys[key(t)] = len(groups)
|
keys[key(t)] = len(groups)
|
||||||
g := ToolGroup{Tool: t, Item: item, Gear: s.gear(t.ID), Count: 1, Reach: item.ToolReach(t.Tier)}
|
g := ToolGroup{Tool: t, Item: item, Count: 1, Reach: item.ToolReach()}
|
||||||
g.Secs, g.Yield = s.drill(t, item, resource)
|
g.Secs, g.Yield = s.drill(t, item, resource)
|
||||||
groups = append(groups, g)
|
groups = append(groups, g)
|
||||||
}
|
}
|
||||||
@@ -881,30 +881,11 @@ func (s *State) page(v View) *Page {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
row := ToolRow{
|
row := ToolRow{
|
||||||
ToolGroup: ToolGroup{Tool: t, Item: item, Gear: s.gear(t.ID), Count: 1, Reach: item.ToolReach(t.Tier)},
|
ToolGroup: ToolGroup{Tool: t, Item: item, Count: 1, Reach: item.ToolReach()},
|
||||||
Selected: t.ID == v.Drill,
|
Selected: t.ID == v.Drill,
|
||||||
MaxTier: len(item.Upgrades),
|
|
||||||
Next: item.NextUpgrade(t.Tier),
|
|
||||||
}
|
}
|
||||||
row.Secs, row.Yield = s.drill(t, item, "")
|
row.Secs, row.Yield = s.drill(t, item, "")
|
||||||
row.FreeSlots = max(0, cfg().Attachments.SlotsPerTool-len(row.Gear))
|
row.Tracks = s.trackRows(t, item)
|
||||||
row.Ladder = s.tierLadder(t, item)
|
|
||||||
if row.Next != nil {
|
|
||||||
row.Preview = s.upgradePreview(t, item)
|
|
||||||
row.CanAfford = s.Balance >= row.Next.Price
|
|
||||||
if !row.CanAfford {
|
|
||||||
row.Missing = shortBy(row.Next.Price - s.Balance)
|
|
||||||
row.Short = row.Missing
|
|
||||||
}
|
|
||||||
row.Parts = s.upgradeParts(t.StoreItemID, row.Next.Tier)
|
|
||||||
for _, part := range row.Parts {
|
|
||||||
if !part.OK() && row.CanAfford {
|
|
||||||
row.CanAfford = false
|
|
||||||
row.Missing = fmt.Sprintf("Need %s %s (you have %s). Make it in the Refinery",
|
|
||||||
formatMoney(part.Need), part.Name, formatMoney(part.Have))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if st := s.StakeForTool(t.ID); st != nil {
|
if st := s.StakeForTool(t.ID); st != nil {
|
||||||
sr := s.stakeRow(*st)
|
sr := s.stakeRow(*st)
|
||||||
row.Stake = &sr
|
row.Stake = &sr
|
||||||
@@ -926,14 +907,12 @@ func (s *State) page(v View) *Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "store":
|
case "store":
|
||||||
p.Gear = s.attachmentCards()
|
|
||||||
best := s.bestToolRate()
|
best := s.bestToolRate()
|
||||||
for i := range s.Config.StoreItems {
|
for i := range s.Config.StoreItems {
|
||||||
item := &s.Config.StoreItems[i]
|
item := &s.Config.StoreItems[i]
|
||||||
row := StoreRow{Item: item, CanAfford: s.Balance >= item.Price, BestRate: best}
|
row := StoreRow{Item: item, CanAfford: s.Balance >= item.Price, BestRate: best}
|
||||||
secs, yield := drillStats(item.CompletionTimeInSecs, item.ToolYield(0), s.Bonus(),
|
secs, yield := s.drillAt(item, Levels{}, "")
|
||||||
s.CycleBoosts.For("all"), s.YieldBoosts.For(""))
|
row.Rate = s.creditsPerHour(yield, secs, item.ToolReach())
|
||||||
row.Rate = s.creditsPerHour(yield, secs, item.ToolReach(0))
|
|
||||||
for _, t := range s.Tools {
|
for _, t := range s.Tools {
|
||||||
if t.StoreItemID == item.ID {
|
if t.StoreItemID == item.ID {
|
||||||
row.Owned++
|
row.Owned++
|
||||||
@@ -970,22 +949,9 @@ func (s *State) page(v View) *Page {
|
|||||||
// Busy is how many of the row's copies are mining.
|
// Busy is how many of the row's copies are mining.
|
||||||
func (r ToolRow) Busy() int { return r.Count - r.Idle }
|
func (r ToolRow) Busy() int { return r.Count - r.Idle }
|
||||||
|
|
||||||
// Pips is one entry per attachment slot, true where one is equipped.
|
// copyKey is the same for copies of a tool: same type and track levels.
|
||||||
func (r ToolRow) Pips() []bool {
|
|
||||||
pips := make([]bool, len(r.Gear)+r.FreeSlots)
|
|
||||||
for i := range r.Gear {
|
|
||||||
pips[i] = true
|
|
||||||
}
|
|
||||||
return pips
|
|
||||||
}
|
|
||||||
|
|
||||||
// copyKey is the same for copies of a tool: same type, tier and attachments.
|
|
||||||
func (s *State) copyKey(t Tool) string {
|
func (s *State) copyKey(t Tool) string {
|
||||||
var names []string
|
return fmt.Sprint(t.StoreItemID, t.Levels())
|
||||||
for _, g := range s.gear(t.ID) {
|
|
||||||
names = append(names, g.Name)
|
|
||||||
}
|
|
||||||
return fmt.Sprint(t.StoreItemID, t.Tier, names)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// idleCopy is an idle copy of a busy tool (the tool itself if it's idle or has none), so
|
// idleCopy is an idle copy of a busy tool (the tool itself if it's idle or has none), so
|
||||||
@@ -1059,7 +1025,7 @@ func (p *Page) StakeURL(st *StakeRow) string {
|
|||||||
type BuyModal struct {
|
type BuyModal struct {
|
||||||
ToolID string
|
ToolID string
|
||||||
Item *StoreItem
|
Item *StoreItem
|
||||||
Secs int
|
Secs float64
|
||||||
Yield float64
|
Yield float64
|
||||||
Reach int
|
Reach int
|
||||||
Suggestions []Suggestion
|
Suggestions []Suggestion
|
||||||
@@ -1087,9 +1053,9 @@ func (s *State) buySuggestions(toolID string) *BuyModal {
|
|||||||
if item == nil {
|
if item == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
m := &BuyModal{ToolID: toolID, Item: item, NoMoons: len(s.Moons) == 0, Reach: item.ToolReach(t.Tier)}
|
m := &BuyModal{ToolID: toolID, Item: item, NoMoons: len(s.Moons) == 0, Reach: item.ToolReach()}
|
||||||
m.Secs, m.Yield = s.drill(*t, item, "")
|
m.Secs, m.Yield = s.drill(*t, item, "")
|
||||||
cycle := float64(m.Secs) / 60
|
cycle := m.Secs / 60
|
||||||
var all []Suggestion
|
var all []Suggestion
|
||||||
for i := range s.Moons {
|
for i := range s.Moons {
|
||||||
moon := &s.Moons[i]
|
moon := &s.Moons[i]
|
||||||
@@ -1149,93 +1115,13 @@ func (s *State) buySuggestions(toolID string) *BuyModal {
|
|||||||
// ReachText says how many wells the tool mines ("" for one).
|
// ReachText says how many wells the tool mines ("" for one).
|
||||||
func (m *BuyModal) ReachText() string { return reachLabel(m.Reach) }
|
func (m *BuyModal) ReachText() string { return reachLabel(m.Reach) }
|
||||||
|
|
||||||
// ------------------------------------------------------------------ upgrade previews
|
|
||||||
|
|
||||||
// UpgradePreview is what a tool's next tier changes, with the player's bonuses now.
|
|
||||||
type UpgradePreview struct {
|
|
||||||
Tier int
|
|
||||||
YieldFrom, YieldTo float64
|
|
||||||
SecsFrom, SecsTo int
|
|
||||||
ReachFrom, ReachTo int
|
|
||||||
RateFrom, RateTo float64 // credits per hour (creditsPerHour)
|
|
||||||
Cost float64 // price plus the parts at sell value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UpgradePreview) RateGain() float64 { return u.RateTo - u.RateFrom }
|
|
||||||
|
|
||||||
// Payback is how long the extra income takes to repay the cost, "" if it never does.
|
|
||||||
func (u *UpgradePreview) Payback() string {
|
|
||||||
if gain := u.RateGain(); gain > 0 {
|
|
||||||
return formatSpan(u.Cost / gain * 3600)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// TierRow is one tier of a tool, for the drill panel's ladder.
|
|
||||||
type TierRow struct {
|
|
||||||
Tier, Secs, Reach int
|
|
||||||
Yield, Rate float64
|
|
||||||
Current, Next bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// tierStats is what a stake of the tool would take and return at the given tier, with its
|
|
||||||
// attachments, the player's advisors and live events (s.drill at its own tier).
|
|
||||||
func (s *State) tierStats(t Tool, item *StoreItem, tier int) (int, float64, int) {
|
|
||||||
secs, yield := drillStats(item.ToolSecs(tier), item.ToolYield(tier), s.toolBonus(t.ID),
|
|
||||||
s.CycleBoosts.For("all"), s.YieldBoosts.For(""))
|
|
||||||
return secs, yield, item.ToolReach(tier)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) upgradePreview(t Tool, item *StoreItem) *UpgradePreview {
|
|
||||||
next := item.NextUpgrade(t.Tier)
|
|
||||||
if next == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
u := &UpgradePreview{Tier: next.Tier, Cost: next.Price + partsValue(cfg(), upgradeParts(item.ID, next.Tier))}
|
|
||||||
u.SecsFrom, u.YieldFrom, u.ReachFrom = s.tierStats(t, item, t.Tier)
|
|
||||||
u.SecsTo, u.YieldTo, u.ReachTo = s.tierStats(t, item, next.Tier)
|
|
||||||
u.RateFrom = s.creditsPerHour(u.YieldFrom, u.SecsFrom, u.ReachFrom)
|
|
||||||
u.RateTo = s.creditsPerHour(u.YieldTo, u.SecsTo, u.ReachTo)
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *State) tierLadder(t Tool, item *StoreItem) []TierRow {
|
|
||||||
var rows []TierRow
|
|
||||||
for tier := 0; tier <= len(item.Upgrades); tier++ {
|
|
||||||
r := TierRow{Tier: tier, Current: tier == t.Tier, Next: tier == t.Tier+1}
|
|
||||||
r.Secs, r.Yield, r.Reach = s.tierStats(t, item, tier)
|
|
||||||
r.Rate = s.creditsPerHour(r.Yield, r.Secs, r.Reach)
|
|
||||||
rows = append(rows, r)
|
|
||||||
}
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|
||||||
// upgradedText names what a tool's latest upgrade gave, for the toast.
|
|
||||||
func (s *State) upgradedText(t Tool) string {
|
|
||||||
item := s.StoreItem(t.StoreItemID)
|
|
||||||
if item == nil || t.Tier < 1 {
|
|
||||||
return "Tool upgraded"
|
|
||||||
}
|
|
||||||
_, before, reachFrom := s.tierStats(t, item, t.Tier-1)
|
|
||||||
_, yield, reach := s.tierStats(t, item, t.Tier)
|
|
||||||
msg := fmt.Sprintf("Upgraded to T%d: +%s per cycle", t.Tier, formatMoney(yield))
|
|
||||||
if yield > before {
|
|
||||||
msg += " (was +" + formatMoney(before) + ")"
|
|
||||||
}
|
|
||||||
if reach > reachFrom {
|
|
||||||
label := reachLabel(reach)
|
|
||||||
msg += ", now " + strings.ToLower(label[:1]) + label[1:]
|
|
||||||
}
|
|
||||||
return msg
|
|
||||||
}
|
|
||||||
|
|
||||||
// bestToolRate is what the player's best-earning tool makes per hour (creditsPerHour).
|
// bestToolRate is what the player's best-earning tool makes per hour (creditsPerHour).
|
||||||
func (s *State) bestToolRate() float64 {
|
func (s *State) bestToolRate() float64 {
|
||||||
best := 0.0
|
best := 0.0
|
||||||
for _, t := range s.Tools {
|
for _, t := range s.Tools {
|
||||||
if item := s.StoreItem(t.StoreItemID); item != nil {
|
if item := s.StoreItem(t.StoreItemID); item != nil {
|
||||||
secs, yield, reach := s.tierStats(t, item, t.Tier)
|
secs, yield := s.drill(t, item, "")
|
||||||
best = max(best, s.creditsPerHour(yield, secs, reach))
|
best = max(best, s.creditsPerHour(yield, secs, item.ToolReach()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return best
|
return best
|
||||||
@@ -1245,7 +1131,7 @@ func (s *State) bestToolRate() float64 {
|
|||||||
// default grade (State.MoonGrade), deployed on its
|
// default grade (State.MoonGrade), deployed on its
|
||||||
// richest well (what its reach can hold, as the economy page's sim counts it), sold at
|
// richest well (what its reach can hold, as the economy page's sim counts it), sold at
|
||||||
// the average resource price now (market, live events, advisors).
|
// the average resource price now (market, live events, advisors).
|
||||||
func (s *State) creditsPerHour(yield float64, secs, reach int) float64 {
|
func (s *State) creditsPerHour(yield, secs float64, reach int) float64 {
|
||||||
c := cfg()
|
c := cfg()
|
||||||
if len(c.ResourcePrices) == 0 {
|
if len(c.ResourcePrices) == 0 {
|
||||||
return 0
|
return 0
|
||||||
@@ -1295,7 +1181,7 @@ func formatCompact(n float64) string {
|
|||||||
func formatSpan(secs float64) string {
|
func formatSpan(secs float64) string {
|
||||||
n := int(math.Ceil(secs))
|
n := int(math.Ceil(secs))
|
||||||
if n < 3600 {
|
if n < 3600 {
|
||||||
return formatCycle(n)
|
return formatCycle(float64(n))
|
||||||
}
|
}
|
||||||
h, m := n/3600, n%3600/60
|
h, m := n/3600, n%3600/60
|
||||||
if m == 0 {
|
if m == 0 {
|
||||||
@@ -1498,7 +1384,7 @@ func (r StakeRow) Pct() float64 {
|
|||||||
if r.Ready || r.DurationInSecs <= 0 {
|
if r.Ready || r.DurationInSecs <= 0 {
|
||||||
return 100
|
return 100
|
||||||
}
|
}
|
||||||
return min(100, max(0, 100*(1-r.Remaining.Seconds()/float64(r.DurationInSecs))))
|
return min(100, max(0, 100*(1-r.Remaining.Seconds()/r.DurationInSecs)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code is the tool's model code, the last word of its name ("PD-1").
|
// Code is the tool's model code, the last word of its name ("PD-1").
|
||||||
@@ -1524,7 +1410,7 @@ func (r StakeRow) Fate() string {
|
|||||||
if r.Item != nil {
|
if r.Item != nil {
|
||||||
name = r.Item.Name
|
name = r.Item.Name
|
||||||
}
|
}
|
||||||
s := fmt.Sprintf("%s T%d", name, r.Tier)
|
s := name + r.Levels.Tag()
|
||||||
if r.Reach > 1 {
|
if r.Reach > 1 {
|
||||||
s += " (" + reachLabel(r.Reach) + ")"
|
s += " (" + reachLabel(r.Reach) + ")"
|
||||||
}
|
}
|
||||||
@@ -1548,7 +1434,7 @@ func (r StakeRow) Fate() string {
|
|||||||
func (r WellRow) DrillsTitle() string { return drillsTitle(r.Queue()) }
|
func (r WellRow) DrillsTitle() string { return drillsTitle(r.Queue()) }
|
||||||
func (g DrillGroup) DrillsTitle() string { return drillsTitle(g.Drills) }
|
func (g DrillGroup) DrillsTitle() string { return drillsTitle(g.Drills) }
|
||||||
|
|
||||||
// DrillLabel names a row's own drills compactly ("PD-1 T1", "PD-1 ×3", "PD-1 ×3 +1");
|
// DrillLabel names a row's own drills compactly ("PD-1 Y1 C2", "PD-1 ×3", "PD-1 ×3 +1");
|
||||||
// a well's multi-well drills show only as diamond-capped bars.
|
// a well's multi-well drills show only as diamond-capped bars.
|
||||||
func (r WellRow) DrillLabel() string { return drillLabel(r.Drills) }
|
func (r WellRow) DrillLabel() string { return drillLabel(r.Drills) }
|
||||||
func (g DrillGroup) DrillLabel() string { return drillLabel(g.Drills) }
|
func (g DrillGroup) DrillLabel() string { return drillLabel(g.Drills) }
|
||||||
@@ -1566,7 +1452,7 @@ func drillLabel(q []StakeRow) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if len(q) == 1 {
|
if len(q) == 1 {
|
||||||
return fmt.Sprintf("%s T%d", q[0].Code(), q[0].Tier)
|
return q[0].Code() + q[0].Levels.Tag()
|
||||||
}
|
}
|
||||||
count := map[string]int{}
|
count := map[string]int{}
|
||||||
top := q[0].Code()
|
top := q[0].Code()
|
||||||
@@ -1647,3 +1533,14 @@ type SupplyBar struct {
|
|||||||
func (r WellRow) Bar(max float64, now time.Time, panel bool) SupplyBar {
|
func (r WellRow) Bar(max float64, now time.Time, panel bool) SupplyBar {
|
||||||
return SupplyBar{W: r, Max: max, Now: now, Panel: panel}
|
return SupplyBar{W: r, Max: max, Now: now, Panel: panel}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// secsText is formatCycle for a template value, int or float64 seconds.
|
||||||
|
func secsText(v any) string {
|
||||||
|
switch n := v.(type) {
|
||||||
|
case int:
|
||||||
|
return formatCycle(float64(n))
|
||||||
|
case float64:
|
||||||
|
return formatCycle(n)
|
||||||
|
}
|
||||||
|
return fmt.Sprint(v)
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A tool's panel opens as soon as Equipment is unlocked; its Store parts (upgrade,
|
// A tool's panel opens as soon as Equipment is unlocked; its Store parts (upgrades,
|
||||||
// attachments, selling) show only once the Store is.
|
// selling) show only once the Store is.
|
||||||
func TestViewDrillPanelOnboarding(t *testing.T) {
|
func TestViewDrillPanelOnboarding(t *testing.T) {
|
||||||
tmpl, err := (&server{}).parseTemplates()
|
tmpl, err := (&server{}).parseTemplates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -44,10 +44,10 @@ func TestViewDrillPanelOnboarding(t *testing.T) {
|
|||||||
t.Fatalf("render: %v", err)
|
t.Fatalf("render: %v", err)
|
||||||
}
|
}
|
||||||
html := b.String()
|
html := b.String()
|
||||||
if !strings.Contains(html, "<h3>Tiers</h3>") {
|
if strings.Count(html, "track-ladder") != 2 {
|
||||||
t.Errorf("%v: no tier ladder", c.flags)
|
t.Errorf("%v: no track ladders", c.flags)
|
||||||
}
|
}
|
||||||
for _, part := range []string{"/act/sell-tool", "<h3>Slots</h3>", "Spare attachments"} {
|
for _, part := range []string{"/act/sell-tool", "/act/upgrade"} {
|
||||||
if strings.Contains(html, part) != c.store {
|
if strings.Contains(html, part) != c.store {
|
||||||
t.Errorf("%v: %q shown %v, want %v", c.flags, part, !c.store, c.store)
|
t.Errorf("%v: %q shown %v, want %v", c.flags, part, !c.store, c.store)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function left(ms) {
|
|||||||
|
|
||||||
// Cycles: 3s, 2m, 2m 30s.
|
// Cycles: 3s, 2m, 2m 30s.
|
||||||
export function secs(n) {
|
export function secs(n) {
|
||||||
|
if (n < 60 && n !== Math.trunc(n)) return `${Math.round(n * 10) / 10}s`; // tenths, as formatCycle
|
||||||
n = Math.round(n);
|
n = Math.round(n);
|
||||||
if (n < 60) return `${n}s`;
|
if (n < 60) return `${n}s`;
|
||||||
return n % 60 === 0 ? `${n / 60}m` : `${Math.floor(n / 60)}m ${n % 60}s`;
|
return n % 60 === 0 ? `${n / 60}m` : `${Math.floor(n / 60)}m ${n % 60}s`;
|
||||||
@@ -52,11 +53,18 @@ export function hue(id) {
|
|||||||
// The tool's model code, the last word of its name ("PD-1").
|
// The tool's model code, the last word of its name ("PD-1").
|
||||||
export const code = (name) => name?.trim().split(/\s+/).pop() || "Tool";
|
export const code = (name) => name?.trim().split(/\s+/).pop() || "Tool";
|
||||||
|
|
||||||
// A row's drills, compactly ("PD-1 T1", "PD-1 ×3", "PD-1 ×3 +1"), like drillLabel in views.go.
|
// A row's drills, compactly ("PD-1 Y1 C2", "PD-1 ×3", "PD-1 ×3 +1"), like drillLabel in views.go.
|
||||||
|
// A tool's track levels after its name (" Y2 C1"), "" for a base tool: Levels.Tag in tracks.go.
|
||||||
|
export function levelsTag(t) {
|
||||||
|
const y = t?.yieldLevel ?? 0;
|
||||||
|
const c = t?.cycleLevel ?? 0;
|
||||||
|
return y || c ? ` Y${y} C${c}` : "";
|
||||||
|
}
|
||||||
|
|
||||||
export function drillLabel(drills, toolOf) {
|
export function drillLabel(drills, toolOf) {
|
||||||
if (!drills.length) return "";
|
if (!drills.length) return "";
|
||||||
const codeOf = (d) => toolOf(d.toolId)?.code ?? "Tool";
|
const codeOf = (d) => toolOf(d.toolId)?.code ?? "Tool";
|
||||||
if (drills.length === 1) return `${codeOf(drills[0])} T${toolOf(drills[0].toolId)?.tier ?? 0}`;
|
if (drills.length === 1) return codeOf(drills[0]) + levelsTag(toolOf(drills[0].toolId));
|
||||||
const count = new Map();
|
const count = new Map();
|
||||||
let top = codeOf(drills[0]);
|
let top = codeOf(drills[0]);
|
||||||
for (const d of drills) {
|
for (const d of drills) {
|
||||||
|
|||||||
@@ -70,16 +70,17 @@ function addItem(st, name, delta) {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- tools and stakes
|
// ---------------------------------------------------------------- tools and stakes
|
||||||
|
|
||||||
// The tool to deploy for id: it, or an idle copy of it (same store item and tier).
|
// The tool to deploy for id: it, or an idle copy of it (same store item and track levels).
|
||||||
function idleCopy(st, id) {
|
function idleCopy(st, id) {
|
||||||
const t = find(st.tools, id);
|
const t = find(st.tools, id);
|
||||||
if (!t || !t.stakeId) return t;
|
if (!t || !t.stakeId) return t;
|
||||||
return st.tools.find((x) => !x.stakeId && x.storeItemId === t.storeItemId && x.tier === t.tier);
|
return st.tools.find((x) => !x.stakeId && x.storeItemId === t.storeItemId && x.yieldLevel === t.yieldLevel && x.cycleLevel === t.cycleLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
const yieldBoost = (st, resource) => st.rules?.boosts?.yield?.[resource] ?? 1;
|
const yieldBoost = (st, resource) => st.rules?.boosts?.yield?.[resource] ?? 1;
|
||||||
|
|
||||||
const sameGroup = (g, tool) => g.storeItemId === tool.storeItemId && g.tier === tool.tier;
|
const sameGroup = (g, tool) =>
|
||||||
|
g.storeItemId === tool.storeItemId && g.yieldLevel === tool.yieldLevel && g.cycleLevel === tool.cycleLevel;
|
||||||
|
|
||||||
// A new stake of tool on well starting at ctx.now. group: the well panel's idle group for
|
// A new stake of tool on well starting at ctx.now. group: the well panel's idle group for
|
||||||
// it (cycle and yield already for this well).
|
// it (cycle and yield already for this well).
|
||||||
@@ -153,7 +154,8 @@ function removeStake(st, s) {
|
|||||||
toolId: tool.id,
|
toolId: tool.id,
|
||||||
storeItemId: tool.storeItemId,
|
storeItemId: tool.storeItemId,
|
||||||
name: tool.name,
|
name: tool.name,
|
||||||
tier: tool.tier,
|
yieldLevel: tool.yieldLevel,
|
||||||
|
cycleLevel: tool.cycleLevel,
|
||||||
count: 1,
|
count: 1,
|
||||||
cycleSecs: tool.cycleSecs,
|
cycleSecs: tool.cycleSecs,
|
||||||
yield: y,
|
yield: y,
|
||||||
@@ -161,7 +163,6 @@ function removeStake(st, s) {
|
|||||||
here: Math.min(y, free),
|
here: Math.min(y, free),
|
||||||
free,
|
free,
|
||||||
short: free < y,
|
short: free < y,
|
||||||
attachments: tool.attachments ?? [],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ function predictWell(st, w) {
|
|||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
for (const t of st.tools) {
|
for (const t of st.tools) {
|
||||||
if (t.stakeId) continue;
|
if (t.stakeId) continue;
|
||||||
const key = `${t.storeItemId}:${t.tier}:${(t.attachments ?? []).join("+")}`;
|
const key = `${t.storeItemId}:${t.yieldLevel}:${t.cycleLevel}`;
|
||||||
const g = groups.get(key);
|
const g = groups.get(key);
|
||||||
if (g) {
|
if (g) {
|
||||||
g.count++;
|
g.count++;
|
||||||
@@ -330,7 +330,8 @@ function predictWell(st, w) {
|
|||||||
toolId: t.id,
|
toolId: t.id,
|
||||||
storeItemId: t.storeItemId,
|
storeItemId: t.storeItemId,
|
||||||
name: t.name,
|
name: t.name,
|
||||||
tier: t.tier,
|
yieldLevel: t.yieldLevel,
|
||||||
|
cycleLevel: t.cycleLevel,
|
||||||
count: 1,
|
count: 1,
|
||||||
cycleSecs: t.cycleSecs,
|
cycleSecs: t.cycleSecs,
|
||||||
yield: y,
|
yield: y,
|
||||||
@@ -338,7 +339,6 @@ function predictWell(st, w) {
|
|||||||
here: Math.min(y, free),
|
here: Math.min(y, free),
|
||||||
free,
|
free,
|
||||||
short: free < y,
|
short: free < y,
|
||||||
attachments: t.attachments ?? [],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const quote = st.market && P.find(st.market.quotes, w.resource, "item");
|
const quote = st.market && P.find(st.market.quotes, w.resource, "item");
|
||||||
|
|||||||
+8
-13
@@ -7,7 +7,7 @@ import { Countdown, Dot, Num, SupplyBar } from "./ui.jsx";
|
|||||||
import { ReadyButton } from "./buttons.jsx";
|
import { ReadyButton } from "./buttons.jsx";
|
||||||
import { ViewLink, WideClaim, wideOn } from "./moons.jsx";
|
import { ViewLink, WideClaim, wideOn } from "./moons.jsx";
|
||||||
import { colorOf, imageOf, toolOf } from "./catalog.js";
|
import { colorOf, imageOf, toolOf } from "./catalog.js";
|
||||||
import { money, cur, secs } from "./format.js";
|
import { money, cur, secs, levelsTag } from "./format.js";
|
||||||
import { useFlip } from "./flip.js";
|
import { useFlip } from "./flip.js";
|
||||||
import { now } from "./clock.js";
|
import { now } from "./clock.js";
|
||||||
import { isReady, find } from "./predict.js";
|
import { isReady, find } from "./predict.js";
|
||||||
@@ -88,7 +88,7 @@ function WellPanel({ st, moon, well, w }) {
|
|||||||
{well.idle.length ? (
|
{well.idle.length ? (
|
||||||
<div class="deploy-cards" ref={cards}>
|
<div class="deploy-cards" ref={cards}>
|
||||||
{well.idle.map((g) => (
|
{well.idle.map((g) => (
|
||||||
<DeployCard key={`${g.storeItemId}:${g.tier}:${(g.attachments ?? []).join("+")}`} st={st} g={g} wellId={well.id} />
|
<DeployCard key={`${g.storeItemId}:${g.yieldLevel}:${g.cycleLevel}`} st={st} g={g} wellId={well.id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -122,7 +122,8 @@ function WellPanel({ st, moon, well, w }) {
|
|||||||
<span class="drill-thumb">{tool && <img src={tool.image} alt="" />}</span>
|
<span class="drill-thumb">{tool && <img src={tool.image} alt="" />}</span>
|
||||||
<span class="drill-main">
|
<span class="drill-main">
|
||||||
<span class="drill-name">
|
<span class="drill-name">
|
||||||
{tool?.name ?? "Tool"} <span class="muted">T{tool?.tier ?? 0}</span>
|
{tool?.name ?? "Tool"}
|
||||||
|
{levelsTag(tool) && <span class="muted">{levelsTag(tool)}</span>}
|
||||||
</span>
|
</span>
|
||||||
<span class="drill-meta small">
|
<span class="drill-meta small">
|
||||||
<span class="muted">
|
<span class="muted">
|
||||||
@@ -192,30 +193,24 @@ function WellPanel({ st, moon, well, w }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DeployCard({ st, g, wellId }) {
|
function DeployCard({ st, g, wellId }) {
|
||||||
const key = `${g.storeItemId}:${g.tier}:${(g.attachments ?? []).join("+")}`;
|
const key = `${g.storeItemId}:${g.yieldLevel}:${g.cycleLevel}`;
|
||||||
const cls = g.short ? (g.here > 0 ? " is-short" : " is-spoken") : "";
|
const cls = g.short ? (g.here > 0 ? " is-short" : " is-spoken") : "";
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-key={key}
|
data-key={key}
|
||||||
class={`btn deploy-card${cls}`}
|
class={`btn deploy-card${cls}`}
|
||||||
title={`Deploy ${g.name} T${g.tier}`}
|
title={`Deploy ${g.name}${levelsTag(g)}`}
|
||||||
onClick={(e) => act("stake", { tool: g.toolId, target: wellId }, e.currentTarget, `stake:${key}`)}
|
onClick={(e) => act("stake", { tool: g.toolId, target: wellId }, e.currentTarget, `stake:${key}`)}
|
||||||
>
|
>
|
||||||
<span class="deploy-card-media">
|
<span class="deploy-card-media">
|
||||||
<img src={imageOf(st, g.storeItemId)} alt="" />
|
<img src={imageOf(st, g.storeItemId)} alt="" />
|
||||||
{g.attachments?.length > 0 && (
|
|
||||||
<span class="deploy-card-pips pips">
|
|
||||||
{g.attachments.map((a, i) => (
|
|
||||||
<i key={i} class="pip is-on" />
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{g.count > 1 && <span class="deploy-card-count num">×{g.count}</span>}
|
{g.count > 1 && <span class="deploy-card-count num">×{g.count}</span>}
|
||||||
</span>
|
</span>
|
||||||
<span class="deploy-card-info">
|
<span class="deploy-card-info">
|
||||||
<span class="deploy-card-name">
|
<span class="deploy-card-name">
|
||||||
<span class="deploy-card-title">{g.name}</span> <span class="tier-chip num">T{g.tier}</span>
|
<span class="deploy-card-title">{g.name}</span>
|
||||||
|
{levelsTag(g) && <span class="tier-chip num">{levelsTag(g)}</span>}
|
||||||
</span>
|
</span>
|
||||||
<span class="deploy-card-meta">
|
<span class="deploy-card-meta">
|
||||||
<span class="num">
|
<span class="num">
|
||||||
|
|||||||
+74
-68
@@ -1054,28 +1054,11 @@ a.htmx-request::after {
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Drill attachments (store, tool names, drill panel) and advisors. */
|
/* Advisors' effects. */
|
||||||
.mod-tag {
|
|
||||||
margin-left: 6px;
|
|
||||||
padding: 1px 6px;
|
|
||||||
border: 1px solid rgb(var(--violet-rgb) / 0.45);
|
|
||||||
color: var(--violet-hi);
|
|
||||||
font-size: 10px;
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.mod-slot {
|
|
||||||
margin-left: 6px;
|
|
||||||
color: var(--faint);
|
|
||||||
font-size: 11px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.mod-effect {
|
.mod-effect {
|
||||||
color: var(--violet-hi);
|
color: var(--violet-hi);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.gear-card .card-stats,
|
|
||||||
.advisor-card .card-stats {
|
.advisor-card .card-stats {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
@@ -1086,42 +1069,79 @@ a.htmx-request::after {
|
|||||||
.tool-status {
|
.tool-status {
|
||||||
min-height: 1.5em;
|
min-height: 1.5em;
|
||||||
}
|
}
|
||||||
/* Attachment slots and equipped attachments sit on the image's top-left corner. */
|
/* A drill's two upgrade tracks (tracks.go), side by side: name and level, value ▸ next,
|
||||||
.tool-card {
|
the button and a need line. Every box has the same rows in every state, so the buttons
|
||||||
position: relative;
|
keep their place (clicker rule). */
|
||||||
|
.track-ups {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.tool-gear {
|
.track-up {
|
||||||
position: absolute;
|
display: flex;
|
||||||
top: 8px;
|
flex-direction: column;
|
||||||
left: 8px;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.track-up-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--text-2);
|
||||||
|
}
|
||||||
|
.track-up-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.track-up-head .num {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.track-up-val {
|
||||||
|
min-height: 1.5em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.track-up .card-buy {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.track-up .card-buy.is-max {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px 0;
|
justify-content: center;
|
||||||
max-width: calc(100% - 16px);
|
color: var(--muted);
|
||||||
padding: 4px 6px;
|
border-color: var(--line);
|
||||||
background: rgb(var(--card-2-rgb) / 0.82);
|
cursor: default;
|
||||||
pointer-events: none;
|
}
|
||||||
|
.track-up .up-need {
|
||||||
|
min-height: 1.6em;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.track-up .up-need .upgrade-parts {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.track-up .up-short {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tool, store and attachment cards: the render spans the card edge to edge (cropped to
|
/* Tool and store cards: the render spans the card edge to edge (cropped to
|
||||||
its middle) and the info under it stays compact. */
|
its middle) and the info under it stays compact. */
|
||||||
:is(.tool-card, .store-card, .gear-card) .card-media {
|
:is(.tool-card, .store-card) .card-media {
|
||||||
height: auto;
|
height: auto;
|
||||||
aspect-ratio: 4 / 3;
|
aspect-ratio: 4 / 3;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
:is(.tool-card, .store-card, .gear-card) .card-media img {
|
:is(.tool-card, .store-card) .card-media img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
object-position: 50% 30%; /* keep the tops of the tall rigs */
|
object-position: 50% 30%; /* keep the tops of the tall rigs */
|
||||||
}
|
}
|
||||||
:is(.tool-card, .store-card, .gear-card) .card-body {
|
:is(.tool-card, .store-card) .card-body {
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 12px 14px 14px;
|
padding: 12px 14px 14px;
|
||||||
}
|
}
|
||||||
:is(.tool-card, .store-card, .gear-card) h2 {
|
:is(.tool-card, .store-card) h2 {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
}
|
}
|
||||||
@@ -1130,25 +1150,25 @@ a.htmx-request::after {
|
|||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 0 6px;
|
gap: 0 6px;
|
||||||
}
|
}
|
||||||
:is(.tool-card, .store-card, .gear-card) .card-desc {
|
:is(.tool-card, .store-card) .card-desc {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
:is(.tool-card, .store-card, .gear-card) .card-stats {
|
:is(.tool-card, .store-card) .card-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 2px 16px;
|
gap: 2px 16px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
:is(.tool-card, .store-card, .gear-card) .card-stats > div {
|
:is(.tool-card, .store-card) .card-stats > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
:is(.tool-card, .store-card, .gear-card) .card-stats dd {
|
:is(.tool-card, .store-card) .card-stats dd {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
:is(.tool-card, .store-card, .gear-card) .btn.card-buy {
|
:is(.tool-card, .store-card) .btn.card-buy {
|
||||||
height: 34px;
|
height: 34px;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
@@ -1269,18 +1289,13 @@ a.htmx-request::after {
|
|||||||
.tier-ladder .is-next .tl-tier::after {
|
.tier-ladder .is-next .tl-tier::after {
|
||||||
content: " ▸";
|
content: " ▸";
|
||||||
}
|
}
|
||||||
.pips {
|
/* A tool's track levels after its name (" Y2 C1"). */
|
||||||
display: inline-flex;
|
.lv-tag {
|
||||||
gap: 4px;
|
margin-left: 0.4em;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.pip {
|
.track-ladder li {
|
||||||
width: 9px;
|
grid-template-columns: 3em 1fr 1.6fr 3.6em;
|
||||||
height: 9px;
|
|
||||||
border: 1px solid var(--violet);
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
.pip.is-on {
|
|
||||||
background: var(--violet);
|
|
||||||
}
|
}
|
||||||
.advisor-card.is-hired {
|
.advisor-card.is-hired {
|
||||||
border-color: rgb(var(--ready-rgb) / 0.4);
|
border-color: rgb(var(--ready-rgb) / 0.4);
|
||||||
@@ -1410,30 +1425,30 @@ a.adv-seat-in[aria-current] {
|
|||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.tool-card, .store-card, .gear-card) .card-media {
|
:is(.tool-card, .store-card) .card-media {
|
||||||
width: 112px;
|
width: 112px;
|
||||||
aspect-ratio: auto;
|
aspect-ratio: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.tool-card, .store-card, .gear-card) h2 {
|
:is(.tool-card, .store-card) h2 {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.tool-card, .store-card, .gear-card) .card-media img {
|
:is(.tool-card, .store-card) .card-media img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.tool-card, .store-card, .gear-card) .card-body {
|
:is(.tool-card, .store-card) .card-body {
|
||||||
padding: 10px 12px 12px;
|
padding: 10px 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.tool-card, .store-card, .gear-card) .card-stats {
|
:is(.tool-card, .store-card) .card-stats {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 2px 12px;
|
gap: 2px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.tool-card, .store-card, .gear-card) .btn.card-buy {
|
:is(.tool-card, .store-card) .btn.card-buy {
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
@@ -1441,9 +1456,6 @@ a.adv-seat-in[aria-current] {
|
|||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-gear {
|
|
||||||
max-width: 96px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.steps {
|
.steps {
|
||||||
@@ -1583,7 +1595,7 @@ a.adv-seat-in[aria-current] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Idle tools in the well panel: one click deploys. The render fills the card (3:2, a bit
|
/* Idle tools in the well panel: one click deploys. The render fills the card (3:2, a bit
|
||||||
flatter than the equipment cards so two rows fit) with the count and attachments on it; one line of name, one of
|
flatter than the equipment cards so two rows fit) with the count on it; one line of name, one of
|
||||||
stats. Two columns in the side panel, one on narrow screens (image on the left). */
|
stats. Two columns in the side panel, one on narrow screens (image on the left). */
|
||||||
.deploy-cards {
|
.deploy-cards {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1626,7 +1638,6 @@ a.adv-seat-in[aria-current] {
|
|||||||
object-position: 50% 30%; /* keep the tops of the tall rigs */
|
object-position: 50% 30%; /* keep the tops of the tall rigs */
|
||||||
}
|
}
|
||||||
|
|
||||||
.deploy-card-pips,
|
|
||||||
.deploy-card-count {
|
.deploy-card-count {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
@@ -1634,11 +1645,6 @@ a.adv-seat-in[aria-current] {
|
|||||||
background: rgb(var(--card-2-rgb) / 0.82);
|
background: rgb(var(--card-2-rgb) / 0.82);
|
||||||
}
|
}
|
||||||
|
|
||||||
.deploy-card-pips {
|
|
||||||
left: 6px;
|
|
||||||
align-items: center;
|
|
||||||
padding: 5px 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deploy-card-count {
|
.deploy-card-count {
|
||||||
right: 6px;
|
right: 6px;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 195 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB |
@@ -41,7 +41,6 @@ function init() {
|
|||||||
for (const r of doc.storeItems ?? []) origin.set(r, r.id);
|
for (const r of doc.storeItems ?? []) origin.set(r, r.id);
|
||||||
for (const r of doc.refinery?.items ?? []) origin.set(r, r.name);
|
for (const r of doc.refinery?.items ?? []) origin.set(r, r.name);
|
||||||
for (const r of doc.moons ?? []) origin.set(r, r.name);
|
for (const r of doc.moons ?? []) origin.set(r, r.name);
|
||||||
for (const r of doc.attachments?.items ?? []) origin.set(r, r.name);
|
|
||||||
for (const r of doc.advisors?.items ?? []) origin.set(r, r.name);
|
for (const r of doc.advisors?.items ?? []) origin.set(r, r.name);
|
||||||
}
|
}
|
||||||
adopt(draft);
|
adopt(draft);
|
||||||
@@ -82,7 +81,6 @@ function init() {
|
|||||||
};
|
};
|
||||||
const resourceNames = () => Object.keys(draft.resourcePrices ?? {}).sort();
|
const resourceNames = () => Object.keys(draft.resourcePrices ?? {}).sort();
|
||||||
const refinedNames = () => (draft.refinery?.items ?? []).map((i) => i.name);
|
const refinedNames = () => (draft.refinery?.items ?? []).map((i) => i.name);
|
||||||
const attachmentNames = () => (draft.attachments?.items ?? []).map((i) => i.name);
|
|
||||||
const advisorNames = () => (draft.advisors?.items ?? []).map((i) => i.name);
|
const advisorNames = () => (draft.advisors?.items ?? []).map((i) => i.name);
|
||||||
const quote = (name) => (data.market ?? []).find((q) => q.name === name);
|
const quote = (name) => (data.market ?? []).find((q) => q.name === name);
|
||||||
const spark = (q) =>
|
const spark = (q) =>
|
||||||
@@ -229,6 +227,22 @@ function init() {
|
|||||||
{ k: "completionTimeInSecs", label: "Cycle s", type: "int", min: 1 },
|
{ k: "completionTimeInSecs", label: "Cycle s", type: "int", min: 1 },
|
||||||
// Wells of a moon one cycle mines: 0 or 1 one, as many as a moon can have the whole moon.
|
// Wells of a moon one cycle mines: 0 or 1 one, as many as a moon can have the whole moon.
|
||||||
{ k: "reach", label: "Wells/cycle", type: "int", min: 0 },
|
{ k: "reach", label: "Wells/cycle", type: "int", min: 0 },
|
||||||
|
{
|
||||||
|
k: "_yieldTrack",
|
||||||
|
label: "Yield track",
|
||||||
|
type: "text",
|
||||||
|
cls: "w-name",
|
||||||
|
get: (r) => r.tracks?.yield?.name ?? "",
|
||||||
|
set: (r, v) => (((r.tracks ??= {}).yield ??= { name: "", levels: [] }).name = v),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
k: "_cycleTrack",
|
||||||
|
label: "Cycle track",
|
||||||
|
type: "text",
|
||||||
|
cls: "w-name",
|
||||||
|
get: (r) => r.tracks?.cycle?.name ?? "",
|
||||||
|
set: (r, v) => (((r.tracks ??= {}).cycle ??= { name: "", levels: [] }).name = v),
|
||||||
|
},
|
||||||
{ k: "_rate", label: "Units/h", calc: (r) => (r.claimAmount * 3600) / r.completionTimeInSecs },
|
{ k: "_rate", label: "Units/h", calc: (r) => (r.claimAmount * 3600) / r.completionTimeInSecs },
|
||||||
{
|
{
|
||||||
k: "_payback",
|
k: "_payback",
|
||||||
@@ -241,17 +255,16 @@ function init() {
|
|||||||
digits: 1,
|
digits: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sub: {
|
// The two upgrade tracks (tracks.go): each level sets the value flat (a claim, or a
|
||||||
label: "Upgrade tiers",
|
// cycle in seconds), with a price and refined parts.
|
||||||
rows: (r) => (r.upgrades ??= []),
|
subs: ["yield", "cycle"].map((kind) => ({
|
||||||
|
kind,
|
||||||
|
label: kind === "yield" ? "Yield track (claim per cycle)" : "Cycle track (seconds per cycle)",
|
||||||
|
rows: (r) => ((((r.tracks ??= {})[kind] ??= { name: "", levels: [] }).levels ??= [])),
|
||||||
cols: [
|
cols: [
|
||||||
{ k: "tier", label: "Tier", ro: true, type: "int" },
|
{ k: "_level", label: "Level", calc: (u, p) => ((p.tracks?.[kind]?.levels ?? []).indexOf(u) + 1), digits: 0 },
|
||||||
|
{ k: "value", label: kind === "yield" ? "Claim" : "Cycle s", type: "num", min: kind === "yield" ? 0 : 1 },
|
||||||
{ k: "price", label: "Price ₥", type: "num", min: 0 },
|
{ k: "price", label: "Price ₥", type: "num", min: 0 },
|
||||||
{ k: "claimBoost", label: "Claim boost", type: "num", min: 0 },
|
|
||||||
{ k: "_yield", label: "Claim at tier", calc: (u, p) => p.claimAmount + u.claimBoost },
|
|
||||||
{ k: "speed", label: "Speed ×", type: "num", min: 0 },
|
|
||||||
// From this tier on; 0 keeps the tool's (or a lower tier's).
|
|
||||||
{ k: "reach", label: "Wells/cycle", type: "int", min: 0 },
|
|
||||||
{
|
{
|
||||||
k: "parts",
|
k: "parts",
|
||||||
label: "Parts (refined)",
|
label: "Parts (refined)",
|
||||||
@@ -261,9 +274,12 @@ function init() {
|
|||||||
},
|
},
|
||||||
{ k: "_partsValue", label: "Parts value ₥", calc: (u) => Object.entries(u.parts ?? {}).reduce((s, [n, q]) => s + q * unitPrice(draft, n), 0) },
|
{ k: "_partsValue", label: "Parts value ₥", calc: (u) => Object.entries(u.parts ?? {}).reduce((s, [n, q]) => s + q * unitPrice(draft, n), 0) },
|
||||||
],
|
],
|
||||||
newRow: (rows) => ({ tier: rows.length + 1, price: rows.at(-1)?.price ?? 0, claimBoost: rows.at(-1)?.claimBoost ?? 0 }),
|
newRow: (rows, p) => ({
|
||||||
},
|
value: rows.at(-1)?.value ?? (kind === "yield" ? p.claimAmount : p.completionTimeInSecs),
|
||||||
newRow: () => ({ id: nextID(), name: "New tool", description: "", price: 0, image: "", claimAmount: 1, completionTimeInSecs: 3, upgrades: [] }),
|
price: rows.at(-1)?.price ?? 0,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
newRow: () => ({ id: nextID(), name: "New tool", description: "", price: 0, image: "", claimAmount: 1, completionTimeInSecs: 3, tracks: { yield: { name: "", levels: [] }, cycle: { name: "", levels: [] } } }),
|
||||||
copy: (r) => ({ ...clone(r), id: nextID(), name: uniqueName(r.name + " copy", draft.storeItems.map((x) => x.name)) }),
|
copy: (r) => ({ ...clone(r), id: nextID(), name: uniqueName(r.name + " copy", draft.storeItems.map((x) => x.name)) }),
|
||||||
check(row, col, v) {
|
check(row, col, v) {
|
||||||
if (col.k === "id" && draft.storeItems.filter((r) => r.id === v).length > 1) return "Duplicate id";
|
if (col.k === "id" && draft.storeItems.filter((r) => r.id === v).length > 1) return "Duplicate id";
|
||||||
@@ -340,27 +356,8 @@ function init() {
|
|||||||
market: kvTable((path) => /^market/i.test(path[0])),
|
market: kvTable((path) => /^market/i.test(path[0])),
|
||||||
constants: kvTable((path) => !/^market/i.test(path[0])),
|
constants: kvTable((path) => !/^market/i.test(path[0])),
|
||||||
|
|
||||||
// Drill attachments and advisors (bonus.go). Bonuses are multipliers (1.2 = +20%); the
|
// Advisors (bonus.go). Bonuses are multipliers (1.2 = +20%); the caps and seat prices
|
||||||
// caps, slots per tool and seat prices are in Constants.
|
// are in Constants.
|
||||||
attachments: {
|
|
||||||
...arrayTable("name", (d) => ((d.attachments ??= {}).items ??= [])),
|
|
||||||
noun: "attachment",
|
|
||||||
cols: [
|
|
||||||
{ k: "image", label: "Image", type: "img", dir: "attachments" },
|
|
||||||
{ k: "name", label: "Name", type: "text", required: true, cls: "w-name" },
|
|
||||||
{ k: "price", label: "Price ₥", type: "num", min: 0 },
|
|
||||||
{ k: "speed", label: "Speed ×", type: "num", min: 1 },
|
|
||||||
{ k: "yield", label: "Yield ×", type: "num", min: 1 },
|
|
||||||
{ k: "description", label: "Description", type: "long", cls: "w-desc" },
|
|
||||||
],
|
|
||||||
newRow: () => ({ name: uniqueName("New attachment", attachmentNames()), image: "", price: 0, speed: 1, yield: 1, description: "" }),
|
|
||||||
copy: (r) => ({ ...clone(r), name: uniqueName(r.name + " copy", attachmentNames()) }),
|
|
||||||
check(row, col, v) {
|
|
||||||
if (col.k === "name" && attachmentNames().filter((n) => n === v).length > 1) return "Duplicate name";
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
advisors: {
|
advisors: {
|
||||||
...arrayTable("name", (d) => ((d.advisors ??= {}).items ??= [])),
|
...arrayTable("name", (d) => ((d.advisors ??= {}).items ??= [])),
|
||||||
noun: "advisor",
|
noun: "advisor",
|
||||||
@@ -492,8 +489,11 @@ function init() {
|
|||||||
if (!parent) return table.match(doc, row);
|
if (!parent) return table.match(doc, row);
|
||||||
const p = table.match(doc, parent);
|
const p = table.match(doc, parent);
|
||||||
if (!p) return null;
|
if (!p) return null;
|
||||||
const i = table.sub.rows(parent).indexOf(row);
|
for (const sub of table.subs) {
|
||||||
return (p.upgrades ?? [])[i] ?? null;
|
const i = sub.rows(parent).indexOf(row);
|
||||||
|
if (i >= 0) return (p.tracks?.[sub.kind]?.levels ?? [])[i] ?? null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cellState(table, row, col, parent) {
|
function cellState(table, row, col, parent) {
|
||||||
@@ -560,7 +560,7 @@ function init() {
|
|||||||
const hasActions = !table.noAdd;
|
const hasActions = !table.noAdd;
|
||||||
let html = "<thead><tr>";
|
let html = "<thead><tr>";
|
||||||
if (hasFlags) html += '<th class="w-flag" aria-label="Flags"></th>';
|
if (hasFlags) html += '<th class="w-flag" aria-label="Flags"></th>';
|
||||||
if (table.sub) html += '<th class="w-flag" aria-label="Tiers"></th>';
|
if (table.subs) html += '<th class="w-flag" aria-label="Upgrade tracks"></th>';
|
||||||
for (const col of table.cols) {
|
for (const col of table.cols) {
|
||||||
const s = sort?.k === col.k ? (sort.dir > 0 ? " ▲" : " ▼") : "";
|
const s = sort?.k === col.k ? (sort.dir > 0 ? " ▲" : " ▼") : "";
|
||||||
html += `<th class="${col.calc && !col.left ? "right " : ""}${col.nosort ? "" : "sortable"}" data-sort="${col.k}">${esc(col.label)}${s}</th>`;
|
html += `<th class="${col.calc && !col.left ? "right " : ""}${col.nosort ? "" : "sortable"}" data-sort="${col.k}">${esc(col.label)}${s}</th>`;
|
||||||
@@ -568,23 +568,24 @@ function init() {
|
|||||||
if (hasActions) html += '<th class="w-act"></th>';
|
if (hasActions) html += '<th class="w-act"></th>';
|
||||||
html += "</tr></thead><tbody>";
|
html += "</tr></thead><tbody>";
|
||||||
|
|
||||||
const span = table.cols.length + (hasFlags ? 1 : 0) + (table.sub ? 1 : 0) + (hasActions ? 1 : 0);
|
const span = table.cols.length + (hasFlags ? 1 : 0) + (table.subs ? 1 : 0) + (hasActions ? 1 : 0);
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const fl = hasFlags ? flagFor(table.flagKey(row)) : [];
|
const fl = hasFlags ? flagFor(table.flagKey(row)) : [];
|
||||||
html += `<tr class="${fl.length ? "has-flag" : ""}${origin.has(row) || !table.newRow ? "" : " is-new"}">`;
|
html += `<tr class="${fl.length ? "has-flag" : ""}${origin.has(row) || !table.newRow ? "" : " is-new"}">`;
|
||||||
if (hasFlags) {
|
if (hasFlags) {
|
||||||
html += `<td class="w-flag">${fl.length ? `<span class="eco-flag" title="${esc(fl.join("\n"))}" tabindex="0">!</span>` : ""}</td>`;
|
html += `<td class="w-flag">${fl.length ? `<span class="eco-flag" title="${esc(fl.join("\n"))}" tabindex="0">!</span>` : ""}</td>`;
|
||||||
}
|
}
|
||||||
if (table.sub) {
|
if (table.subs) {
|
||||||
const open = expanded.has(uid(row));
|
const open = expanded.has(uid(row));
|
||||||
html += `<td class="w-flag"><button type="button" class="eco-x" data-act="expand" data-row="${uid(row)}" aria-expanded="${open}" title="Upgrade tiers" aria-label="Upgrade tiers of ${esc(row.name)}">${open ? "▾" : "▸"} ${table.sub.rows(row).length}</button></td>`;
|
const n = table.subs.reduce((s, sub) => s + sub.rows(row).length, 0);
|
||||||
|
html += `<td class="w-flag"><button type="button" class="eco-x" data-act="expand" data-row="${uid(row)}" aria-expanded="${open}" title="Upgrade tracks" aria-label="Upgrade tracks of ${esc(row.name)}">${open ? "▾" : "▸"} ${n}</button></td>`;
|
||||||
}
|
}
|
||||||
for (const col of table.cols) html += cellHTML(table, row, col);
|
for (const col of table.cols) html += cellHTML(table, row, col);
|
||||||
if (hasActions) {
|
if (hasActions) {
|
||||||
html += `<td class="w-act"><button type="button" class="eco-x" data-act="copy" data-row="${uid(row)}" title="Duplicate row" aria-label="Duplicate row">⧉</button><button type="button" class="eco-x eco-del" data-act="delete" data-row="${uid(row)}" title="Delete row" aria-label="Delete row">✕</button></td>`;
|
html += `<td class="w-act"><button type="button" class="eco-x" data-act="copy" data-row="${uid(row)}" title="Duplicate row" aria-label="Duplicate row">⧉</button><button type="button" class="eco-x eco-del" data-act="delete" data-row="${uid(row)}" title="Delete row" aria-label="Delete row">✕</button></td>`;
|
||||||
}
|
}
|
||||||
html += "</tr>";
|
html += "</tr>";
|
||||||
if (table.sub && expanded.has(uid(row))) html += subHTML(table, row, span);
|
if (table.subs && expanded.has(uid(row))) table.subs.forEach((sub, j) => (html += subHTML(table, sub, j, row, span)));
|
||||||
}
|
}
|
||||||
if (!rows.length) html += `<tr><td colspan="${span}" class="muted">No rows${filter ? " match the filter" : ""}.</td></tr>`;
|
if (!rows.length) html += `<tr><td colspan="${span}" class="muted">No rows${filter ? " match the filter" : ""}.</td></tr>`;
|
||||||
html += "</tbody>";
|
html += "</tbody>";
|
||||||
@@ -611,10 +612,9 @@ function init() {
|
|||||||
}${msg ? ` title="${esc(msg)}"` : ""}>${shown ?? ""}${msg ? `<span class="eco-err">${esc(msg)}</span>` : ""}</td>`;
|
}${msg ? ` title="${esc(msg)}"` : ""}>${shown ?? ""}${msg ? `<span class="eco-err">${esc(msg)}</span>` : ""}</td>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function subHTML(table, row, span) {
|
function subHTML(table, sub, j, row, span) {
|
||||||
const sub = table.sub;
|
|
||||||
let html = `<tr class="sub"><td colspan="${span}"><div class="eco-sub"><div class="eco-sub-head"><span class="cap">${esc(sub.label)} · ${esc(row.name)}</span>
|
let html = `<tr class="sub"><td colspan="${span}"><div class="eco-sub"><div class="eco-sub-head"><span class="cap">${esc(sub.label)} · ${esc(row.name)}</span>
|
||||||
<button type="button" class="btn btn-sm" data-act="add-tier" data-row="${uid(row)}">+ Add tier</button></div>
|
<button type="button" class="btn btn-sm" data-act="add-tier" data-sub="${j}" data-row="${uid(row)}">+ Add level</button></div>
|
||||||
<table class="eco-grid eco-subgrid"><thead><tr>`;
|
<table class="eco-grid eco-subgrid"><thead><tr>`;
|
||||||
for (const col of sub.cols) html += `<th class="${col.calc ? "right" : ""}">${esc(col.label)}</th>`;
|
for (const col of sub.cols) html += `<th class="${col.calc ? "right" : ""}">${esc(col.label)}</th>`;
|
||||||
html += '<th class="w-act"></th></tr></thead><tbody>';
|
html += '<th class="w-act"></th></tr></thead><tbody>';
|
||||||
@@ -622,9 +622,9 @@ function init() {
|
|||||||
rows.forEach((u, i) => {
|
rows.forEach((u, i) => {
|
||||||
html += "<tr>";
|
html += "<tr>";
|
||||||
for (const col of sub.cols) html += cellHTML(table, u, col, row);
|
for (const col of sub.cols) html += cellHTML(table, u, col, row);
|
||||||
html += `<td class="w-act"><button type="button" class="eco-x eco-del" data-act="delete-tier" data-row="${uid(row)}" data-index="${i}" title="Delete tier" aria-label="Delete tier ${u.tier}">✕</button></td></tr>`;
|
html += `<td class="w-act"><button type="button" class="eco-x eco-del" data-act="delete-tier" data-sub="${j}" data-row="${uid(row)}" data-index="${i}" title="Delete level" aria-label="Delete level ${i + 1}">✕</button></td></tr>`;
|
||||||
});
|
});
|
||||||
if (!rows.length) html += `<tr><td colspan="${sub.cols.length + 1}" class="muted">No upgrade tiers.</td></tr>`;
|
if (!rows.length) html += `<tr><td colspan="${sub.cols.length + 1}" class="muted">No levels.</td></tr>`;
|
||||||
return html + "</tbody></table></div></td></tr>";
|
return html + "</tbody></table></div></td></tr>";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,16 +810,17 @@ function init() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "add-tier": {
|
case "add-tier": {
|
||||||
const tiers = table.sub.rows(row);
|
const sub = table.subs[Number(btn.dataset.sub)];
|
||||||
tiers.push(table.sub.newRow(tiers));
|
const levels = sub.rows(row);
|
||||||
|
levels.push(sub.newRow(levels, row));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "delete-tier": {
|
case "delete-tier": {
|
||||||
const tiers = table.sub.rows(row);
|
const sub = table.subs[Number(btn.dataset.sub)];
|
||||||
|
const levels = sub.rows(row);
|
||||||
const i = Number(btn.dataset.index);
|
const i = Number(btn.dataset.index);
|
||||||
if (!(await confirmDialog(`Delete tier ${tiers[i].tier} of “${row.name}”? Higher tiers move down one.`, { title: "Delete tier", ok: "Delete", danger: true }))) return;
|
if (!(await confirmDialog(`Delete ${sub.kind} level ${i + 1} of “${row.name}”? Higher levels move down one.`, { title: "Delete level", ok: "Delete", danger: true }))) return;
|
||||||
tiers.splice(i, 1);
|
levels.splice(i, 1);
|
||||||
tiers.forEach((u, j) => (u.tier = j + 1));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -868,7 +869,7 @@ function init() {
|
|||||||
$("#eco-status").innerHTML = msg ? `<span class="${error ? "eco-bad" : "eco-ok"}">${esc(msg)}</span>` : "";
|
$("#eco-status").innerHTML = msg ? `<span class="${error ? "eco-bad" : "eco-ok"}">${esc(msg)}</span>` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server messages name rows like "store item 3 tier 2: ..." or `refinery "Void Core": ...`.
|
// Server messages name rows like "store item 3 yield level 2: ..." or `refinery "Void Core": ...`.
|
||||||
function mapErrors(list) {
|
function mapErrors(list) {
|
||||||
rowErrors = {};
|
rowErrors = {};
|
||||||
generalErrors = [];
|
generalErrors = [];
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ function play(kind, toast, prev, at) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["buy-tool", "upgrade", "upgrade-batch", "buy-moon", "buy-attachment", "equip", "hire", "buy-seat", "seat-advisor", "swap-advisors"].includes(kind)) {
|
if (["buy-tool", "upgrade", "upgrade-batch", "buy-moon", "hire", "buy-seat", "seat-advisor", "swap-advisors"].includes(kind)) {
|
||||||
if (at) burst(at.x, at.y, [CYAN, CYAN_HI], 34);
|
if (at) burst(at.x, at.y, [CYAN, CYAN_HI], 34);
|
||||||
// The page may change under a new moon, so sweep its list entry instead.
|
// The page may change under a new moon, so sweep its list entry instead.
|
||||||
const moon = kind === "buy-moon" && document.querySelector("#moonlist .moon[aria-current]");
|
const moon = kind === "buy-moon" && document.querySelector("#moonlist .moon[aria-current]");
|
||||||
@@ -206,9 +206,6 @@ function play(kind, toast, prev, at) {
|
|||||||
// Claim and sell sound as their numbers arrive (above).
|
// Claim and sell sound as their numbers arrive (above).
|
||||||
const sound = {
|
const sound = {
|
||||||
"buy-tool": "buy",
|
"buy-tool": "buy",
|
||||||
"buy-attachment": "buy",
|
|
||||||
equip: "upgrade",
|
|
||||||
unequip: "toggle",
|
|
||||||
hire: "confirm",
|
hire: "confirm",
|
||||||
"seat-advisor": "upgrade",
|
"seat-advisor": "upgrade",
|
||||||
"unseat-advisor": "toggle",
|
"unseat-advisor": "toggle",
|
||||||
|
|||||||
@@ -356,17 +356,17 @@
|
|||||||
{{end}}{{end}}
|
{{end}}{{end}}
|
||||||
|
|
||||||
{{define "equipment-section"}}
|
{{define "equipment-section"}}
|
||||||
{{$open := .S.Unlocked "store"}}{{/* upgrades, attachments and buying (onboarding.go) */}}
|
{{$open := .S.Unlocked "store"}}{{/* upgrades and buying (onboarding.go) */}}
|
||||||
<div class="heading"><div class="grow"><h1>Equipment</h1><p class="muted">{{plural (len .S.Tools) "tool"}}.{{if $open}} Pick one to equip attachments.{{end}}</p></div>
|
<div class="heading"><div class="grow"><h1>Equipment</h1><p class="muted">{{plural (len .S.Tools) "tool"}}.{{if $open}} Each drill has two upgrade tracks: more per cycle, or shorter cycles.{{end}}</p></div>
|
||||||
{{if $open}}<a href="{{.V.ToSection "store"}}" hx-get="{{.V.ToSection "store"}}" class="btn">Buy tools</a>{{end}}</div>
|
{{if $open}}<a href="{{.V.ToSection "store"}}" hx-get="{{.V.ToSection "store"}}" class="btn">Buy tools</a>{{end}}</div>
|
||||||
<div class="cards">
|
<div class="cards">
|
||||||
{{range .Equipment}}
|
{{range .Equipment}}
|
||||||
{{$row := .}}
|
{{$row := .}}
|
||||||
{{$d := $.V.ToDrill .ID}}
|
{{$d := $.V.ToDrill .ID}}
|
||||||
<article class="card tool-card {{if .Selected}}is-selected{{end}}">
|
<article class="card tool-card {{if .Selected}}is-selected{{end}}">
|
||||||
<a class="card-media" href="{{$d}}" hx-get="{{$d}}" aria-label="{{.Item.Name}}: {{if $open}}attachments{{else}}details{{end}}"><img src="/static/assets/items/{{.Item.Image}}" alt="" onerror="this.style.visibility='hidden'"></a>
|
<a class="card-media" href="{{$d}}" hx-get="{{$d}}" aria-label="{{.Item.Name}}: {{if $open}}upgrades{{else}}details{{end}}"><img src="/static/assets/items/{{.Item.Image}}" alt="" onerror="this.style.visibility='hidden'"></a>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2><a href="{{$d}}" hx-get="{{$d}}" class="row-link">{{.Item.Name}}</a>{{if gt .Count 1}} <span class="muted num">×{{.Count}}</span>{{end}} <span class="tier-chip num" title="Tier">T{{.Tier}}/{{.MaxTier}}</span></h2>
|
<h2><a href="{{$d}}" hx-get="{{$d}}" class="row-link">{{.Item.Name}}</a>{{if gt .Count 1}} <span class="muted num">×{{.Count}}</span>{{end}}{{template "levels-chip" .Tool}}</h2>
|
||||||
<p class="tool-status small">
|
<p class="tool-status small">
|
||||||
{{with .Stake}}
|
{{with .Stake}}
|
||||||
{{$url := $.StakeURL .}}
|
{{$url := $.StakeURL .}}
|
||||||
@@ -379,10 +379,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{{with .ReachText}}<p class="muted small">{{.}} per cycle</p>{{end}}
|
{{with .ReachText}}<p class="muted small">{{.}} per cycle</p>{{end}}
|
||||||
<dl class="card-stats">{{template "tool-stats" .}}</dl>
|
<dl class="card-stats">{{template "tool-stats" .}}</dl>
|
||||||
{{if $open}}
|
{{if $open}}{{template "track-ups" .}}{{end}}
|
||||||
<p class="tool-gear small"><span class="pips" title="Attachment slots">{{range .Pips}}<i class="pip {{if .}}is-on{{end}}"></i>{{end}}</span>{{template "gear" .Gear}}</p>
|
|
||||||
{{template "tool-upgrade" .}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{{else}}
|
{{else}}
|
||||||
@@ -515,7 +512,7 @@
|
|||||||
{{define "mk-tape-items"}}{{range .Quotes}}<a class="mk-tick" href="{{.URL}}" hx-get="{{.URL}}" tabindex="-1"><span class="mk-chip" style="{{color .Resource}}"></span><b class="num">{{.Symbol}}</b> <span class="num">{{cur .Price}}</span> <span class="num {{if .Up}}mk-up{{else}}mk-down{{end}}">{{if .Up}}▲{{else}}▼{{end}} {{.ChangeText}}</span></a>{{end}}{{end}}
|
{{define "mk-tape-items"}}{{range .Quotes}}<a class="mk-tick" href="{{.URL}}" hx-get="{{.URL}}" tabindex="-1"><span class="mk-chip" style="{{color .Resource}}"></span><b class="num">{{.Symbol}}</b> <span class="num">{{cur .Price}}</span> <span class="num {{if .Up}}mk-up{{else}}mk-down{{end}}">{{if .Up}}▲{{else}}▼{{end}} {{.ChangeText}}</span></a>{{end}}{{end}}
|
||||||
|
|
||||||
{{define "store-section"}}
|
{{define "store-section"}}
|
||||||
<div class="heading"><div class="grow"><h1>Store</h1><p class="muted">Most tools mine one well at a time; the biggest mine several wells of a moon, or the whole moon, each cycle. Equip attachments in Equipment.</p></div></div>
|
<div class="heading"><div class="grow"><h1>Store</h1><p class="muted">Most tools mine one well at a time; the biggest mine several wells of a moon, or the whole moon, each cycle. Upgrade them in Equipment.</p></div></div>
|
||||||
<h2 class="refinery-label cap">Tools</h2>
|
<h2 class="refinery-label cap">Tools</h2>
|
||||||
<div class="cards">
|
<div class="cards">
|
||||||
{{range .Store}}
|
{{range .Store}}
|
||||||
@@ -536,29 +533,10 @@
|
|||||||
</article>
|
</article>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{with .Gear}}
|
|
||||||
<h2 class="refinery-label cap">Attachments</h2>
|
|
||||||
<div class="cards">
|
|
||||||
{{range .}}
|
|
||||||
<article class="card gear-card">
|
|
||||||
<div class="card-media"><img src="/static/assets/attachments/{{.Image}}" alt="" onerror="this.style.visibility='hidden'"></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<h2>{{.Name}}</h2>
|
|
||||||
<p class="mod-effect num">{{.Effect}}</p>
|
|
||||||
<p class="card-desc">{{.Description}}</p>
|
|
||||||
<dl class="card-stats">
|
|
||||||
<div><dt class="cap">Spare</dt><dd class="num">{{.Spare}}</dd></div>
|
|
||||||
<div><dt class="cap">Equipped</dt><dd class="num">{{.Equipped}}</dd></div>
|
|
||||||
</dl>
|
|
||||||
<button class="btn btn-primary card-buy" hx-post="/act/buy-attachment" hx-vals="{{vals "item" .Name}}" {{if not .CanAfford}}disabled title="{{short $.S.Balance .Price}}"{{end}}>Buy · <span class="num">{{cur .Price}}</span></button>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "gear"}}{{range .}} <span class="mod-tag" title="{{.Effect}}">{{.Name}}</span>{{end}}{{end}}
|
{{/* A tool's track levels as a chip after its name ("Y2 C1"), none for a base tool. */}}
|
||||||
|
{{define "levels-chip"}}{{with .Levels.Tag}} <span class="tier-chip num" title="{{$.Levels.Title}}">{{.}}</span>{{end}}{{end}}
|
||||||
|
|
||||||
{{define "bonus-chips"}}
|
{{define "bonus-chips"}}
|
||||||
<p class="bonus-chips small">{{range .}}<span class="bonus-chip num" {{if .Capped}}title="At the cap"{{end}}>{{.Label}} {{.Text}}</span>{{end}}</p>
|
<p class="bonus-chips small">{{range .}}<span class="bonus-chip num" {{if .Capped}}title="At the cap"{{end}}>{{.Label}} {{.Text}}</span>{{end}}</p>
|
||||||
@@ -665,7 +643,7 @@
|
|||||||
<li>Pick a well on your moon and deploy a tool on it.</li>
|
<li>Pick a well on your moon and deploy a tool on it.</li>
|
||||||
<li>When the drill finishes, claim what it dug up. "Claim and redeploy" puts the tool straight back to work; after a claim, "Redrill" does the same.</li>
|
<li>When the drill finishes, claim what it dug up. "Claim and redeploy" puts the tool straight back to work; after a claim, "Redrill" does the same.</li>
|
||||||
<li>Sell resources in the Market, upgrade your equipment, claim more moons.</li>
|
<li>Sell resources in the Market, upgrade your equipment, claim more moons.</li>
|
||||||
<li>Buy attachments in the Store and equip them on a drill (click it in Equipment) to make it faster or bigger. Hire advisors to boost your whole operation.</li>
|
<li>Each drill has two upgrade tracks in Equipment: one raises what a cycle mines, the other makes cycles shorter. Hire advisors to boost your whole operation.</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p class="muted">Your progress is saved on this browser.</p>
|
<p class="muted">Your progress is saved on this browser.</p>
|
||||||
<button class="btn" hx-post="/act/new-game"
|
<button class="btn" hx-post="/act/new-game"
|
||||||
@@ -699,14 +677,13 @@
|
|||||||
{{if .Idle}}
|
{{if .Idle}}
|
||||||
<div class="deploy-cards">
|
<div class="deploy-cards">
|
||||||
{{range .Idle}}
|
{{range .Idle}}
|
||||||
<button type="button" class="btn deploy-card{{if .Short}}{{if gt .Here 0.0}} is-short{{else}} is-spoken{{end}}{{end}}"{{if .Blocked}} disabled{{end}} hx-post="/act/stake" hx-vals="{{vals "tool" .ID "target" $.Well.ID}}" title="Deploy {{.Item.Name}} T{{.Tier}}{{range .Gear}} · {{.Name}}{{end}}">
|
<button type="button" class="btn deploy-card{{if .Short}}{{if gt .Here 0.0}} is-short{{else}} is-spoken{{end}}{{end}}"{{if .Blocked}} disabled{{end}} hx-post="/act/stake" hx-vals="{{vals "tool" .ID "target" $.Well.ID}}" title="Deploy {{.Item.Name}}{{.Levels.Tag}}">
|
||||||
<span class="deploy-card-media">
|
<span class="deploy-card-media">
|
||||||
<img src="/static/assets/items/{{.Item.Image}}" alt="" onerror="this.style.visibility='hidden'">
|
<img src="/static/assets/items/{{.Item.Image}}" alt="" onerror="this.style.visibility='hidden'">
|
||||||
{{with .Gear}}<span class="deploy-card-pips pips">{{range .}}<i class="pip is-on"></i>{{end}}</span>{{end}}
|
|
||||||
{{if gt .Count 1}}<span class="deploy-card-count num">×{{.Count}}</span>{{end}}
|
{{if gt .Count 1}}<span class="deploy-card-count num">×{{.Count}}</span>{{end}}
|
||||||
</span>
|
</span>
|
||||||
<span class="deploy-card-info">
|
<span class="deploy-card-info">
|
||||||
<span class="deploy-card-name"><span class="deploy-card-title">{{.Item.Name}}</span> <span class="tier-chip num">T{{.Tier}}</span></span>
|
<span class="deploy-card-name"><span class="deploy-card-title">{{.Item.Name}}</span>{{template "levels-chip" .Tool}}</span>
|
||||||
<span class="deploy-card-meta">
|
<span class="deploy-card-meta">
|
||||||
<span class="num" title="What a cycle gets here after the drills already on it{{if .Short}} (its yield is {{money .Yield}}){{end}}">+{{money .Here}} / {{secs .Secs}}</span>
|
<span class="num" title="What a cycle gets here after the drills already on it{{if .Short}} (its yield is {{money .Yield}}){{end}}">+{{money .Here}} / {{secs .Secs}}</span>
|
||||||
{{if .ReachShort}}<span class="muted" title="{{.ReachText}}">· {{.ReachShort}}</span>{{end}}
|
{{if .ReachShort}}<span class="muted" title="{{.ReachText}}">· {{.ReachShort}}</span>{{end}}
|
||||||
@@ -720,7 +697,7 @@
|
|||||||
{{with .Better}}
|
{{with .Better}}
|
||||||
<div class="line better-well">
|
<div class="line better-well">
|
||||||
<span class="grow small">Better: <a href="{{.URL}}" hx-get="{{.URL}}">{{template "dot" .Resource}}{{.Resource}} well</a>{{if ne .MoonID $.Moon.ID}} on {{.MoonName}}{{end}}, <span class="num">{{money .Free}}</span> free</span>
|
<span class="grow small">Better: <a href="{{.URL}}" hx-get="{{.URL}}">{{template "dot" .Resource}}{{.Resource}} well</a>{{if ne .MoonID $.Moon.ID}} on {{.MoonName}}{{end}}, <span class="num">{{money .Free}}</span> free</span>
|
||||||
<button class="btn btn-sm btn-primary" hx-post="/act/stake" hx-vals="{{vals "tool" .Tool.ID "target" .WellID "s" "moons" "moon" .MoonID "well" .WellID}}" title="Deploy {{.Tool.Item.Name}} T{{.Tool.Tier}} on that well">Deploy there</button>
|
<button class="btn btn-sm btn-primary" hx-post="/act/stake" hx-vals="{{vals "tool" .Tool.ID "target" .WellID "s" "moons" "moon" .MoonID "well" .WellID}}" title="Deploy {{.Tool.Item.Name}}{{.Tool.Levels.Tag}} on that well">Deploy there</button>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
@@ -734,7 +711,7 @@
|
|||||||
<div class="line drill-line is-{{.State}}">
|
<div class="line drill-line is-{{.State}}">
|
||||||
<span class="drill-thumb">{{if .Item}}<img src="/static/assets/items/{{.Item.Image}}" alt="" onerror="this.style.visibility='hidden'">{{end}}</span>
|
<span class="drill-thumb">{{if .Item}}<img src="/static/assets/items/{{.Item.Image}}" alt="" onerror="this.style.visibility='hidden'">{{end}}</span>
|
||||||
<span class="drill-main">
|
<span class="drill-main">
|
||||||
<span class="drill-name">{{if .Item}}{{.Item.Name}}{{else}}Tool{{end}} <span class="muted">T{{.Tier}}</span>{{template "gear" .Gear}}</span>
|
<span class="drill-name">{{if .Item}}{{.Item.Name}}{{else}}Tool{{end}}{{with .Levels.Tag}}<span class="muted lv-tag">{{.}}</span>{{end}}</span>
|
||||||
<span class="drill-meta small"><span class="muted"><span class="num">+{{money .Amount}}</span>/cycle{{with .ReachText}} · {{.}}{{end}}</span>
|
<span class="drill-meta small"><span class="muted"><span class="num">+{{money .Amount}}</span>/cycle{{with .ReachText}} · {{.}}{{end}}</span>
|
||||||
{{if eq .State "none"}}<span class="drill-tag" title="Nothing left by its turn: stops and goes idle when the well runs dry">Nothing left</span>{{else}}<span class="drill-gets">· Gets <span class="num">{{money .Here}}</span>{{if gt .Reach 1}} here{{if ne .Gets .Here}} · <span class="num">{{money .Gets}}</span> in all{{end}}{{end}}{{if eq .State "short"}} of <span class="num">{{money .Amount}}</span>{{end}}</span>{{end}}</span>
|
{{if eq .State "none"}}<span class="drill-tag" title="Nothing left by its turn: stops and goes idle when the well runs dry">Nothing left</span>{{else}}<span class="drill-gets">· Gets <span class="num">{{money .Here}}</span>{{if gt .Reach 1}} here{{if ne .Gets .Here}} · <span class="num">{{money .Gets}}</span> in all{{end}}{{end}}{{if eq .State "short"}} of <span class="num">{{money .Amount}}</span>{{end}}</span>{{end}}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -780,53 +757,59 @@
|
|||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{/* Side panel for a drill picked in the Equipment section (attachments.go): its slots,
|
{{/* A tool's stats (a ToolRow), for a <dl>: a stake's yield and cycle now. */}}
|
||||||
and the spare attachments with the drill's stats once equipped. */}}
|
|
||||||
{{/* A tool's stats (a ToolRow), for a <dl>: each one's value now and, when the next tier
|
|
||||||
changes it, "▸ new value" in the stat-up accent. */}}
|
|
||||||
{{define "tool-stats"}}
|
{{define "tool-stats"}}
|
||||||
{{$u := .Preview}}
|
<div><dt class="cap">Yield</dt><dd class="num">{{money .Yield}}</dd></div>
|
||||||
{{with $u}}<div class="su-tier"><dt class="cap">Tier</dt><dd class="num">T{{$.Tier}} <span class="su" title="After the upgrade">▸ <b>T{{.Tier}}</b></span></dd></div>{{end}}
|
<div><dt class="cap">Cycle</dt><dd class="num">{{secs .Secs}}</dd></div>
|
||||||
<div><dt class="cap">Yield</dt><dd class="num">{{money .Yield}}{{if and $u (gt $u.YieldTo $u.YieldFrom)}} <span class="su" title="After the upgrade">▸ <b>{{money $u.YieldTo}}</b></span>{{end}}</dd></div>
|
|
||||||
{{if and $u (gt $u.ReachTo $u.ReachFrom)}}<div><dt class="cap">Reach</dt><dd class="num">{{$u.ReachFrom}} <span class="su" title="After the upgrade">▸ <b>{{wells $u.ReachTo}}</b></span> <span class="su-new">New</span></dd></div>{{end}}
|
|
||||||
<div><dt class="cap">Cycle</dt><dd class="num">{{secs .Secs}}{{if and $u (ne $u.SecsTo $u.SecsFrom)}} <span class="su" title="After the upgrade">▸ <b>{{secs $u.SecsTo}}</b></span>{{end}}</dd></div>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{/* A tool's next upgrade (a ToolRow): the button and its parts. */}}
|
{{/* A ToolRow's two upgrade tracks side by side (Equipment card, drill panel), each with
|
||||||
{{define "tool-upgrade"}}
|
its level, its value now ▸ after the next level, the button and a need line. Every
|
||||||
|
box has the same rows whatever its state, so the buttons never move (clicker rule). */}}
|
||||||
|
{{define "track-ups"}}{{$id := .ID}}
|
||||||
|
<div class="track-ups">
|
||||||
|
{{range .Tracks}}
|
||||||
|
<div class="track-up">
|
||||||
|
<p class="track-up-head small"><span class="track-up-name">{{.Name}}</span> <span class="num muted" title="Level {{.Level}} of {{.Max}}">{{.Level}}/{{.Max}}</span></p>
|
||||||
|
<p class="track-up-val num small">{{trackval .Cycle .Value}}{{if .Next}} <span class="su" title="After the upgrade">▸ <b>{{trackval .Cycle .NextValue}}</b></span>{{end}}</p>
|
||||||
{{if .Next}}
|
{{if .Next}}
|
||||||
<button class="btn btn-sm card-buy {{if .CanAfford}}su-ready{{end}}" hx-post="/act/upgrade" hx-vals="{{vals "tool" .ID}}" {{if not .CanAfford}}disabled{{with .Missing}} title="{{.}}"{{end}}{{end}}>Upgrade to T{{.Next.Tier}} · <span class="num">{{cur .Next.Price}}</span></button>
|
<button class="btn btn-sm card-buy {{if .CanAfford}}su-ready{{end}}" hx-post="/act/upgrade" hx-vals="{{vals "tool" $id "track" .Kind}}" title="{{.Label}}{{with .Missing}}: {{.}}{{end}}" {{if not .CanAfford}}disabled{{end}}>Upgrade · <span class="num">{{cur .Next.Price}}</span></button>
|
||||||
{{with .Short}}<p class="up-short small num">{{.}}</p>{{end}}
|
<div class="up-need small">{{with .Parts}}{{template "upgrade-parts" .}}{{else}}{{with .Short}}<span class="up-short num">{{.}}</span>{{else}} {{end}}{{end}}</div>
|
||||||
{{with .Parts}}{{template "upgrade-parts" .}}{{end}}
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="muted small card-buy">Max tier</p>
|
<p class="btn btn-sm card-buy is-max" aria-disabled="true">Max level</p>
|
||||||
|
<div class="up-need small"> </div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{/* Side panel for a drill picked in the Equipment section (tracks.go): its stats, both
|
||||||
|
upgrade tracks with their ladders, an idle copy to deploy and selling it. */}}
|
||||||
{{define "drill-panel"}}
|
{{define "drill-panel"}}
|
||||||
<h2>{{.Item.Name}} <span class="muted">T{{.Tier}}</span></h2>
|
<h2>{{.Item.Name}}{{with .Levels.Tag}}<span class="muted lv-tag">{{.}}</span>{{end}}</h2>
|
||||||
<dl class="facts su-facts">{{template "tool-stats" .ToolRow}}</dl>
|
<dl class="facts su-facts">{{template "tool-stats" .ToolRow}}</dl>
|
||||||
{{/* Before the Store is unlocked: stats, tiers and deploying only (DrillPanel.Store). */}}
|
{{/* Before the Store is unlocked: stats, ladders and deploying only (DrillPanel.Store). */}}
|
||||||
{{if .Store}}
|
{{if .Store}}<div class="drill-upgrade">{{template "track-ups" .ToolRow}}</div>{{end}}
|
||||||
{{with .Preview}}{{if gt .RateGain 0.0}}<p class="su-income small num">Income {{curk .RateFrom}}/h ▸ {{curk .RateTo}}/h{{with .Payback}} · pays back in {{.}}{{end}}</p>{{end}}{{end}}
|
{{range .Tracks}}
|
||||||
<div class="drill-upgrade">{{template "tool-upgrade" .ToolRow}}</div>
|
<h3>{{.Name}} <span class="muted small">{{if .Cycle}}cycle{{else}}yield per cycle{{end}}</span></h3>
|
||||||
{{end}}
|
{{if and $.Store .Next (gt .RateGain 0.0)}}<p class="su-income small num">Next level: income {{curk .RateFrom}}/h ▸ {{curk .RateTo}}/h{{with .Payback}} · pays back in {{.}}{{end}}</p>{{end}}
|
||||||
{{with .Ladder}}
|
<ol class="tier-ladder track-ladder num small">
|
||||||
<h3>Tiers</h3>
|
<li class="tl-head cap" aria-hidden="true"><span>Level</span><span>{{if .Cycle}}Cycle{{else}}Yield{{end}}</span><span>Price</span><span class="tl-rate">₥/h</span></li>
|
||||||
<ol class="tier-ladder num small">
|
{{$cycle := .Cycle}}
|
||||||
<li class="tl-head cap" aria-hidden="true"><span>Tier</span><span>Yield</span><span>Reach</span><span>Cycle</span><span class="tl-rate">₥/h</span></li>
|
{{range .Ladder}}
|
||||||
{{range .}}
|
|
||||||
<li class="{{if .Current}}is-current{{else if .Next}}is-next{{end}}"{{if .Current}} aria-current="true"{{end}}>
|
<li class="{{if .Current}}is-current{{else if .Next}}is-next{{end}}"{{if .Current}} aria-current="true"{{end}}>
|
||||||
<span class="tl-tier">T{{.Tier}}</span><span>{{money .Yield}}</span><span>{{wells .Reach}}</span><span>{{secs .Secs}}</span><span class="tl-rate">{{curk .Rate}}</span>
|
<span class="tl-tier">{{.Level}}</span><span>{{trackval $cycle .Value}}</span>
|
||||||
|
<span>{{if .Price}}{{cur .Price}}{{range .Parts}} <span title="{{.Name}}">+{{money .Need}} {{template "dot" .Name}}</span>{{end}}{{else}}—{{end}}</span><span class="tl-rate">{{curk .Rate}}</span>
|
||||||
</li>
|
</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ol>
|
</ol>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Mining}}<p class="muted small">Mining now.{{if .Store}} Changes apply from its next drill.{{end}}</p>{{end}}
|
{{if .Mining}}<p class="muted small">Mining now.{{if .Store}} Upgrades apply from its next drill.{{end}}</p>{{end}}
|
||||||
{{with .IdleBest}}
|
{{with .IdleBest}}
|
||||||
{{$b := $.IdleURL}}
|
{{$b := $.IdleURL}}
|
||||||
<div class="line">
|
<div class="line">
|
||||||
<span class="grow"><a href="{{$b}}" hx-get="{{$b}}">{{$.IdleSame}} idle {{$.Item.Name}}</a> <span class="muted">T{{.Tier}}</span><br>
|
<span class="grow"><a href="{{$b}}" hx-get="{{$b}}">{{$.IdleSame}} idle {{$.Item.Name}}</a>{{with .Levels.Tag}}<span class="muted lv-tag">{{.}}</span>{{end}}<br>
|
||||||
<span class="muted small">{{with $.DeployTo}}Best well: {{.Resource}} on {{.MoonName}}{{else}}No well to deploy on{{end}}</span></span>
|
<span class="muted small">{{with $.DeployTo}}Best well: {{.Resource}} on {{.MoonName}}{{else}}No well to deploy on{{end}}</span></span>
|
||||||
{{with $.DeployTo}}
|
{{with $.DeployTo}}
|
||||||
<button class="btn btn-sm btn-primary" hx-post="/act/stake"
|
<button class="btn btn-sm btn-primary" hx-post="/act/stake"
|
||||||
@@ -836,37 +819,10 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .Store}}
|
{{if .Store}}
|
||||||
<h3>Slots</h3>
|
|
||||||
{{range .Slots}}
|
|
||||||
<div class="line">
|
|
||||||
<span class="cap num">{{.N}}</span>
|
|
||||||
{{with .Gear}}
|
|
||||||
<span class="grow">{{.Name}}<br><span class="muted small">{{.Effect}}</span></span>
|
|
||||||
<button class="btn btn-sm" hx-post="/act/unequip" hx-vals="{{vals "item" .Name "to" $.ID}}">Unequip</button>
|
|
||||||
{{else}}
|
|
||||||
<span class="grow muted">Empty</span>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<h3>Spare attachments</h3>
|
|
||||||
{{range .Spares}}
|
|
||||||
<div class="line">
|
|
||||||
<span class="grow">{{.Name}}{{if gt .Count 1}} <span class="muted">×{{.Count}}</span>{{end}}<br>
|
|
||||||
<span class="muted small num">{{.Effect}} · then {{secs .Secs}}, +{{money .Yield}}</span></span>
|
|
||||||
<button class="btn btn-sm btn-primary" hx-post="/act/equip" hx-vals="{{vals "item" .Name "to" $.ID}}" {{if not .CanEquip}}disabled title="{{.Why}}"{{end}}>Equip</button>
|
|
||||||
<button class="btn btn-sm btn-danger" hx-post="/act/sell-attachment" hx-vals="{{vals "item" .Name}}"
|
|
||||||
hx-confirm="{{.Confirm}}" data-confirm-title="Sell {{.Name}}" data-confirm-ok="Sell for {{cur .Resale}}" data-confirm-danger
|
|
||||||
title="Sell one for {{cur .Resale}}">Sell</button>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<p class="muted small">No spare attachments. <a href="/?s=store" hx-get="/?s=store">Buy some in the store</a>.</p>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<h3>Sell drill</h3>
|
<h3>Sell drill</h3>
|
||||||
<div class="line">
|
<div class="line">
|
||||||
<span class="grow">Sells for <span class="num">{{cur .Resale}}</span><br>
|
<span class="grow">Sells for <span class="num">{{cur .Resale}}</span><br>
|
||||||
<span class="muted small">Part of what it and its upgrades cost. Refined parts aren't refunded{{with .Gear}}; its attachments go back to your inventory{{end}}.</span></span>
|
<span class="muted small">Part of what it and its upgrades cost. Refined parts aren't refunded.</span></span>
|
||||||
<button class="btn btn-sm btn-danger" hx-post="/act/sell-tool" hx-vals="{{vals "tool" .ID}}"
|
<button class="btn btn-sm btn-danger" hx-post="/act/sell-tool" hx-vals="{{vals "tool" .ID}}"
|
||||||
hx-confirm="{{.SellConfirm}}" data-confirm-title="Sell drill" data-confirm-ok="Sell for {{cur .Resale}}" data-confirm-danger
|
hx-confirm="{{.SellConfirm}}" data-confirm-title="Sell drill" data-confirm-ok="Sell for {{cur .Resale}}" data-confirm-danger
|
||||||
{{with .NoSale}}disabled title="{{.}}"{{end}}>Sell</button>
|
{{with .NoSale}}disabled title="{{.}}"{{end}}>Sell</button>
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
<a href="#resources" data-tab="resources">Resources</a>
|
<a href="#resources" data-tab="resources">Resources</a>
|
||||||
<a href="#refined" data-tab="refined">Refined</a>
|
<a href="#refined" data-tab="refined">Refined</a>
|
||||||
<a href="#market" data-tab="market">Market</a>
|
<a href="#market" data-tab="market">Market</a>
|
||||||
<a href="#attachments" data-tab="attachments">Attachments</a>
|
|
||||||
<a href="#advisors" data-tab="advisors">Advisors</a>
|
<a href="#advisors" data-tab="advisors">Advisors</a>
|
||||||
<a href="#moons" data-tab="moons">Moons</a>
|
<a href="#moons" data-tab="moons">Moons</a>
|
||||||
<a href="#constants" data-tab="constants">Constants</a>
|
<a href="#constants" data-tab="constants">Constants</a>
|
||||||
@@ -185,16 +184,16 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Upgrades · claim per cycle / payback of the extra income (price + parts at sell value)</h3>
|
<h3>Upgrade tracks · each level's value (the other track at 0) / payback of the extra income over the level below (price + parts at sell value)</h3>
|
||||||
<div class="table-box">
|
<div class="table-box">
|
||||||
<table class="table eco-table">
|
<table class="table eco-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Tools}}
|
{{range .Tools}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{.Name}}</td>
|
<td>{{.Name}}</td>
|
||||||
{{range .Tiers}}
|
{{range .Levels}}
|
||||||
<td class="num right{{if .TooBig}} eco-red{{end}}" title="Tier {{.Tier}}: {{cur .Price}}{{if .PartsValue}} + parts worth {{cur .PartsValue}}{{end}}, {{.Reach}} well(s), {{money .Eff}} on a moon, {{cur .CreditsPerHour}}/h">
|
<td class="num right{{if .TooBig}} eco-red{{end}}" title="{{.Track}} level {{.Level}}: {{cur .Price}}{{if .PartsValue}} + parts worth {{cur .PartsValue}}{{end}}, {{money .Eff}} on a moon, {{cur .CreditsPerHour}}/h">
|
||||||
T{{.Tier}} {{money .Yield}} <span class="muted">/ {{.Payback}}</span>
|
{{if eq .Track "cycle"}}C{{.Level}} {{secs .Secs}}{{else}}Y{{.Level}} {{money .Yield}}{{end}} <span class="muted">/ {{.Payback}}</span>
|
||||||
</td>
|
</td>
|
||||||
{{end}}
|
{{end}}
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -184,10 +184,12 @@
|
|||||||
hx-confirm="Give this tool to {{.P.Name}}?" data-confirm-ok="Give">
|
hx-confirm="Give this tool to {{.P.Name}}?" data-confirm-ok="Give">
|
||||||
<label class="grow"><span class="label">Tool</span>
|
<label class="grow"><span class="label">Tool</span>
|
||||||
<select class="input" name="item">
|
<select class="input" name="item">
|
||||||
{{range .StoreItems}}<option value="{{.ID}}">#{{.ID}} {{.Name}} (tiers 0–{{len .Upgrades}})</option>{{end}}
|
{{range .StoreItems}}<option value="{{.ID}}">#{{.ID}} {{.Name}} (yield 0–{{len .Tracks.Yield.Levels}}, cycle 0–{{len .Tracks.Cycle.Levels}})</option>{{end}}
|
||||||
</select></label>
|
</select></label>
|
||||||
<label><span class="label">Tier</span>
|
<label><span class="label">Yield level</span>
|
||||||
<input class="input pl-tier" type="number" name="tier" min="0" step="1" value="0" required></label>
|
<input class="input pl-tier" type="number" name="yield" min="0" step="1" value="0" required></label>
|
||||||
|
<label><span class="label">Cycle level</span>
|
||||||
|
<input class="input pl-tier" type="number" name="cycle" min="0" step="1" value="0" required></label>
|
||||||
<button class="btn btn-primary">Give</button>
|
<button class="btn btn-primary">Give</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -219,12 +221,12 @@
|
|||||||
<h2 id="pl-tools-title">Tools</h2>
|
<h2 id="pl-tools-title">Tools</h2>
|
||||||
<div class="table-box">
|
<div class="table-box">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead><tr><th>Tool</th><th>Tier</th><th>Status</th><th class="right">Time left</th></tr></thead>
|
<thead><tr><th>Tool</th><th>Levels</th><th>Status</th><th class="right">Time left</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Tools}}
|
{{range .Tools}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{with .Item}}{{.Name}}{{else}}Item #{{.StoreItemID}}{{end}} <span class="muted num small">{{slice .ID 0 8}}</span></td>
|
<td>{{with .Item}}{{.Name}}{{else}}Item #{{.StoreItemID}}{{end}} <span class="muted num small">{{slice .ID 0 8}}</span></td>
|
||||||
<td class="num">{{.Tier}} / {{.MaxTier}}</td>
|
<td class="num" title="Yield level, cycle level (of the top)">Y{{.YieldLevel}}/{{.Max.Yield}} · C{{.CycleLevel}}/{{.Max.Cycle}}</td>
|
||||||
<td>{{with .Stake}}{{template "dot" .Resource}}{{.Resource}} on {{$.State.MoonName .MoonID}}{{else}}<span class="muted">Idle</span>{{end}}</td>
|
<td>{{with .Stake}}{{template "dot" .Resource}}{{.Resource}} on {{$.State.MoonName .MoonID}}{{else}}<span class="muted">Idle</span>{{end}}</td>
|
||||||
<td class="num right">{{if .Stake}}{{if .Remaining}}{{dur .Remaining}}{{else}}<span class="ready">Ready</span>{{end}}{{else}}—{{end}}</td>
|
<td class="num right">{{if .Stake}}{{if .Remaining}}{{dur .Remaining}}{{else}}<span class="ready">Ready</span>{{end}}{{else}}—{{end}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in new issue
Block a user