Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e18f99796 | ||
|
|
eda38810cd | ||
|
|
91c71fd9c8 | ||
|
|
55f28caa14 | ||
|
|
71dc1d1b1d | ||
|
|
2e6872a824 | ||
|
|
fa18e8c1ed | ||
|
|
a199fdce0c | ||
|
|
5bc0a3deab |
No files matched your search
@@ -699,21 +699,40 @@ M.register("redrill", function(st, p, ctx)
|
|||||||
if #tools == 0 then
|
if #tools == 0 then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
-- Until the well has no room left (the server leaves the rest idle).
|
-- Until the well has no room left, like the server's redrill (game.go redrillPick):
|
||||||
local staked = 0
|
-- the smallest drill that fills what is left alone, else the biggest and again; the
|
||||||
for _, t in ipairs(tools) do
|
-- rest stay idle.
|
||||||
local group = nil
|
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 g.storeItemId == t.storeItemId and g.tier == t.tier then
|
||||||
group = g
|
return g
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local amount = group and group.yield or t.yield * yield_boost(st, w.resource)
|
end
|
||||||
if (M.room(st, id, amount, 1) or 0) <= EPS then
|
local staked = 0
|
||||||
|
while #tools > 0 do
|
||||||
|
local left = M.room(st, id, 1e18, 1) or 0
|
||||||
|
if left <= EPS then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
local pick
|
||||||
|
for i, t in ipairs(tools) do
|
||||||
|
if t.yield >= left - EPS and (not pick or t.yield < tools[pick].yield) then
|
||||||
|
pick = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not pick then
|
||||||
|
pick = 1
|
||||||
|
for i, t in ipairs(tools) do
|
||||||
|
if t.yield > tools[pick].yield then
|
||||||
|
pick = i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local t = table.remove(tools, pick)
|
||||||
|
local group = group_of(t)
|
||||||
M.add_stake(st, t, moon.id, w, ctx, group)
|
M.add_stake(st, t, moon.id, w, ctx, group)
|
||||||
staked = staked + 1
|
staked = staked + 1
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
-- upgrade button with its parts. A card opens the drill panel (view drill=<tool id>) in
|
-- upgrade button with its parts. A card opens the drill panel (view drill=<tool id>) in
|
||||||
-- the aside: stats, upgrade, tier ladder, slots (unequip), spare attachments (equip, with
|
-- the aside: stats, upgrade, tier ladder, slots (unequip), spare attachments (equip, with
|
||||||
-- the stats they'd give; sell), idle copies and their best well, and selling the drill.
|
-- the stats they'd give; sell), idle copies and their best well, and selling the drill.
|
||||||
|
-- Before the Store is unlocked (onboarding) cards and panel leave out the Store's parts
|
||||||
|
-- (attachments, upgrades, selling): the panel is stats, tiers and idle copies.
|
||||||
|
|
||||||
local theme = require("modules.theme")
|
local theme = require("modules.theme")
|
||||||
local ui = require("modules.ui")
|
local ui = require("modules.ui")
|
||||||
@@ -205,6 +207,10 @@ local function tool_card(self, H, st, c, t, open)
|
|||||||
ui.rect(p, 0, 0, bw, 1, "line")
|
ui.rect(p, 0, 0, bw, 1, "line")
|
||||||
return 6 + stats(st, p, t, 0, 4, bw) + 4
|
return 6 + stats(st, p, t, 0, 4, bw) + 4
|
||||||
end)
|
end)
|
||||||
|
c.id = "drill:" .. t.id
|
||||||
|
c.click = function()
|
||||||
|
H.navigate(self, drill_view(self, t.id))
|
||||||
|
end
|
||||||
if open then
|
if open then
|
||||||
-- Attachments: slot pips over the image's corner, tags under the stats.
|
-- Attachments: slot pips over the image's corner, tags under the stats.
|
||||||
local holder = ui.rect(c.root, 6, 6, 1, 22, "panel")
|
local holder = ui.rect(c.root, 6, 6, 1, 22, "panel")
|
||||||
@@ -223,10 +229,6 @@ local function tool_card(self, H, st, c, t, open)
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
c.id = "drill:" .. t.id
|
|
||||||
c.click = function()
|
|
||||||
H.navigate(self, drill_view(self, t.id))
|
|
||||||
end
|
|
||||||
-- Measure the upgrade block on a probe; the footer sits at the row's bottom.
|
-- 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 = upgrade(self, { action = function() end }, st, probe, t, 0, 0, c.bw, true)
|
||||||
@@ -344,6 +346,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 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")
|
||||||
@@ -368,7 +371,7 @@ function M.aside(self, root, w, h, H)
|
|||||||
fg.label(c, "T" .. t.tier, { font = "mono", px = 13, color = "muted" }, 20),
|
fg.label(c, "T" .. t.tier, { 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, "Mining now. Changes apply from its next drill.", { font = "sans", px = 13, color = "muted" }, 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)
|
||||||
end) or nil,
|
end) or nil,
|
||||||
}),
|
}),
|
||||||
fg.button(c, "×", { small = true, w = 32 }, function(b)
|
fg.button(c, "×", { small = true, w = 32 }, function(b)
|
||||||
@@ -379,7 +382,7 @@ function M.aside(self, root, w, h, H)
|
|||||||
|
|
||||||
-- Stats and the upgrade.
|
-- Stats and the upgrade.
|
||||||
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 = t.preview
|
local u = open and t.preview
|
||||||
if u and u.rateTo > u.rateFrom + 0.5 then
|
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.
|
-- "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" }
|
local m12 = { font = "mono", px = 12, color = "muted" }
|
||||||
@@ -391,8 +394,10 @@ function M.aside(self, root, w, h, H)
|
|||||||
}))
|
}))
|
||||||
add(space(0, 6))
|
add(space(0, 6))
|
||||||
end
|
end
|
||||||
|
if open then
|
||||||
add(space(0, 4))
|
add(space(0, 4))
|
||||||
add(upgrade_box(self, H, st, c, t, inner, false))
|
add(upgrade_box(self, H, st, c, t, inner, false))
|
||||||
|
end
|
||||||
add(space(0, 8))
|
add(space(0, 8))
|
||||||
|
|
||||||
-- Tier ladder.
|
-- Tier ladder.
|
||||||
@@ -426,6 +431,8 @@ function M.aside(self, root, w, h, H)
|
|||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The Store's parts: slots, spare attachments, selling the drill.
|
||||||
|
if open then
|
||||||
-- Slots.
|
-- Slots.
|
||||||
add(h3(c, "Slots"))
|
add(h3(c, "Slots"))
|
||||||
local slots = {}
|
local slots = {}
|
||||||
@@ -565,6 +572,7 @@ function M.aside(self, root, w, h, H)
|
|||||||
end),
|
end),
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
local tree = col({ width = inner }, kids)
|
local tree = col({ width = inner }, kids)
|
||||||
fg.build(flex.solve(tree, pad, pad), c)
|
fg.build(flex.solve(tree, pad, pad), c)
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""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
|
||||||
|
Equipment, a tool card opens its panel (stats, tiers) without the Store's parts (upgrade,
|
||||||
|
slots, spares, Sell drill); after the first claim and sale reveal the Store, the same panel
|
||||||
|
has them. Shots "before" and "after", each linted."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
from lists import admin, call # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def labels(game):
|
||||||
|
"""Every shown text on screen, lower case."""
|
||||||
|
return {(n.get("text") or "").lower() for n in game.dump()["nodes"] if n.get("text")}
|
||||||
|
|
||||||
|
|
||||||
|
def open_panel(game, tool):
|
||||||
|
game.navigate("{section='equipment'}")
|
||||||
|
game.idle()
|
||||||
|
game.click("drill:" + tool)
|
||||||
|
game.wait_for("g.view.drill ~= '' and g.state.equipment.drill ~= nil", 10)
|
||||||
|
game.idle()
|
||||||
|
|
||||||
|
|
||||||
|
def run(game, args):
|
||||||
|
game.idle()
|
||||||
|
admin(game, "credits", amount=5000, op="grant")
|
||||||
|
r = call(game, "POST", "/api/act/buy-moon", {})
|
||||||
|
game.expect(r.get("ok"), "buy-moon: %s" % r.get("error"))
|
||||||
|
st = r["state"]
|
||||||
|
tool = st["tools"][0]["id"]
|
||||||
|
well = st["moon"]["wells"][0]["id"]
|
||||||
|
r = call(game, "POST", "/api/act/stake", {"tool": tool, "target": well})
|
||||||
|
game.expect(r.get("ok"), "stake: %s" % r.get("error"))
|
||||||
|
u = r["state"]["unlocked"]
|
||||||
|
game.expect(u["equipment"] and not u["store"], "after the first deploy: %s" % u)
|
||||||
|
|
||||||
|
open_panel(game, tool)
|
||||||
|
game.sleep(2) # (the wallet counts down from the moon purchase)
|
||||||
|
game.shot("before")
|
||||||
|
game.lint("before")
|
||||||
|
texts = labels(game)
|
||||||
|
game.expect("tiers" in texts, "tier ladder shown")
|
||||||
|
for t in ("slots", "spare attachments", "sell drill"):
|
||||||
|
game.expect(t not in texts, "%s hidden before the Store" % t)
|
||||||
|
game.expect(not any(t.startswith("upgrade to") for t in texts), "no upgrade before the Store")
|
||||||
|
|
||||||
|
# First claim (Market), first sale (Store).
|
||||||
|
admin(game, "finish-stakes")
|
||||||
|
r = call(game, "POST", "/api/act/claim", {"claim-well": well})
|
||||||
|
game.expect(r.get("ok"), "claim: %s" % r.get("error"))
|
||||||
|
res = [x for x in r["state"]["resources"] if x["amount"] > 0][0]["name"]
|
||||||
|
r = call(game, "POST", "/api/act/sell", {"resource": res, "all": "1"})
|
||||||
|
game.expect(r.get("ok"), "sell: %s" % r.get("error"))
|
||||||
|
game.expect(r["state"]["unlocked"]["store"], "store unlocked after the sale")
|
||||||
|
|
||||||
|
open_panel(game, tool)
|
||||||
|
game.sleep(3) # (the sale's effects and the wallet)
|
||||||
|
game.shot("after")
|
||||||
|
game.lint("after")
|
||||||
|
texts = labels(game)
|
||||||
|
for t in ("tiers", "slots", "spare attachments", "sell drill"):
|
||||||
|
game.expect(t in texts, "%s shown after the Store" % t)
|
||||||
+15
-16
@@ -46,20 +46,25 @@ function M:init(moon, on_select, opts)
|
|||||||
-- The name may hold a Greek letter (sym font): drawn as a label at the name's place.
|
-- The name may hold a Greek letter (sym font): drawn as a label at the name's place.
|
||||||
local p = gui.get_position(name)
|
local p = gui.get_position(name)
|
||||||
gui.set_enabled(name, false)
|
gui.set_enabled(name, false)
|
||||||
local _, name_w = ui.label(self.content, moon.name, "moon", p.x, -p.y)
|
ui.label(self.content, moon.name, "moon", p.x, -p.y)
|
||||||
|
-- The second line: grade and rating chips, then the status ("1 drill"); the name has
|
||||||
|
-- the first line to itself and the ready badge the right side.
|
||||||
|
local sp = gui.get_position(sub)
|
||||||
|
local cx = sp.x
|
||||||
if (moon.grade or "") ~= "" then
|
if (moon.grade or "") ~= "" then
|
||||||
local gb = chip.grade(self.content, moon.grade, p.x + name_w + 8, -p.y)
|
local gb = chip.grade(self.content, moon.grade, cx, -sp.y)
|
||||||
tooltip.add(gb, "Grade " .. moon.grade .. " moon")
|
tooltip.add(gb, "Grade " .. moon.grade .. " moon")
|
||||||
name_w = name_w + 8 + chip.GRADE_W
|
cx = cx + chip.GRADE_W + 6
|
||||||
end
|
end
|
||||||
if moon.rating then
|
if moon.rating then
|
||||||
-- The web's .rating-chip: outlined, in the tier's color; exceptional filled, glowing.
|
-- The web's .rating-chip: outlined, in the tier's color; exceptional filled, glowing.
|
||||||
local tier = moon.rating.tier
|
local tier = moon.rating.tier
|
||||||
local color = TIER[tier] or "line_hi"
|
local color = TIER[tier] or "line_hi"
|
||||||
local box = chip.new(self.content, "★" .. moon.rating.stars, p.x + name_w + 8, -p.y, {
|
local box, bw = chip.new(self.content, "★" .. moon.rating.stars, cx, -sp.y, {
|
||||||
color = color, alpha = tier == "good" and 0.5 or nil, text = TIER[tier] or "muted",
|
color = color, alpha = tier == "good" and 0.5 or nil, text = TIER[tier] or "muted",
|
||||||
font = "sym", px = 10, h = 16, pad = 4, fill = tier == "exceptional" and 0.15 or nil,
|
font = "sym", px = 10, h = 16, pad = 4, fill = tier == "exceptional" and 0.15 or nil,
|
||||||
})
|
})
|
||||||
|
cx = cx + bw + 6
|
||||||
self.chip = { box }
|
self.chip = { box }
|
||||||
if tier == "exceptional" then
|
if tier == "exceptional" then
|
||||||
local bs, bp = gui.get_size(box), gui.get_position(box)
|
local bs, bp = gui.get_size(box), gui.get_position(box)
|
||||||
@@ -95,14 +100,17 @@ function M:init(moon, on_select, opts)
|
|||||||
status = moon.drills > 0 and ui.plural(moon.drills, "drill") or (dry and "Tapped out" or "Idle")
|
status = moon.drills > 0 and ui.plural(moon.drills, "drill") or (dry and "Tapped out" or "Idle")
|
||||||
end
|
end
|
||||||
ui.style_text(sub, "small", status)
|
ui.style_text(sub, "small", status)
|
||||||
|
if cx > sp.x then
|
||||||
|
cx = cx + 2 -- (a little more room before the text)
|
||||||
|
end
|
||||||
if moon.over and not moon.finished then
|
if moon.over and not moon.finished then
|
||||||
-- A well here has more drills than supply (the web's warn dot).
|
-- A well here has more drills than supply (the web's warn dot).
|
||||||
local sp = gui.get_position(sub)
|
local dot = ui.rect(self.content, cx, -sp.y - 4, 7, 7, "ready")
|
||||||
local dot = ui.rect(self.content, sp.x, -sp.y - 4, 7, 7, "ready")
|
|
||||||
gui.play_flipbook(dot, "sphere")
|
gui.play_flipbook(dot, "sphere")
|
||||||
tooltip.add(dot, "A well here has more drills than supply")
|
tooltip.add(dot, "A well here has more drills than supply")
|
||||||
gui.set_position(sub, vmath.vector3(sp.x + 12, sp.y, sp.z))
|
cx = cx + 12
|
||||||
end
|
end
|
||||||
|
gui.set_position(sub, vmath.vector3(cx, sp.y, sp.z))
|
||||||
|
|
||||||
-- (opts.ready: the count on our clock, core/ready)
|
-- (opts.ready: the count on our clock, core/ready)
|
||||||
local n_ready = opts.ready or moon.ready or 0
|
local n_ready = opts.ready or moon.ready or 0
|
||||||
@@ -150,15 +158,6 @@ function M:set_size(w)
|
|||||||
end
|
end
|
||||||
local badge = self:get_node("badge")
|
local badge = self:get_node("badge")
|
||||||
gui.set_position(badge, vmath.vector3(w - 10, -h / 2, 0))
|
gui.set_position(badge, vmath.vector3(w - 10, -h / 2, 0))
|
||||||
if self.chip then
|
|
||||||
-- A narrow row: the rating chip goes rather than run into the ready badge.
|
|
||||||
local box = self.chip[1]
|
|
||||||
local right = gui.get_position(box).x + gui.get_size(box).x
|
|
||||||
local limit = w - 16 - (gui.is_enabled(badge) and gui.get_size(badge).x or 0)
|
|
||||||
for _, n in ipairs(self.chip) do
|
|
||||||
gui.set_enabled(n, right <= limit)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The veil's alpha at rest (dimmed, or lifted by hover / selection).
|
-- The veil's alpha at rest (dimmed, or lifted by hover / selection).
|
||||||
|
|||||||
+1
-1
@@ -114,7 +114,7 @@ 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` (B) or higher Refinery, buying a
|
attachments, 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 or attachment 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
|
||||||
|
|||||||
+5
-3
@@ -77,7 +77,7 @@ Core-loop actions and their params:
|
|||||||
| `buy-moon` | `grade` (optional: `D`, `C`, `B`, `A`, `S`; default `state.moonGrade`, the grade of the newest moon, else the lowest) | view moves to the new moon; an unknown grade is a rule error |
|
| `buy-moon` | `grade` (optional: `D`, `C`, `B`, `A`, `S`; default `state.moonGrade`, the grade of the newest moon, else the lowest) | view moves to the new moon; an unknown grade is a rule error |
|
||||||
| `stake` | `tool` (tool id), `target` (well id) | deploy; any idle copy of the tool may be used. Refused ("Nothing left to mine on this well") when the drills already on the moon will take all the tool could get (its deploy card's `here` is 0, `blocked`) |
|
| `stake` | `tool` (tool id), `target` (well id) | deploy; any idle copy of the tool may be used. Refused ("Nothing left to mine on this well") when the drills already on the moon will take all the tool could get (its deploy card's `here` is 0, `blocked`) |
|
||||||
| `claim` | `claim-well` or `claim-moon` or `claim-wide` (moon id), `redeploy=1` | no scope: claim all (needs `unlocked["claim-all"]`); `redeploy` needs `unlocked.redeploy`; a tool whose well is drained or has no room left (as `stake`) stays idle |
|
| `claim` | `claim-well` or `claim-moon` or `claim-wide` (moon id), `redeploy=1` | no scope: claim all (needs `unlocked["claim-all"]`); `redeploy` needs `unlocked.redeploy`; a tool whose well is drained or has no room left (as `stake`) stays idle |
|
||||||
| `redrill` | `redrill-well` (well id) | redeploy idle tools that last mined the well, until it has no room left (as `stake`; the rest stay idle, refused if none fits) |
|
| `redrill` | `redrill-well` (well id) | redeploy idle tools that last mined the well, until it has no room left: each time the smallest that fills what is left alone, else the biggest (as `stake`; the rest stay idle, refused if none fits) |
|
||||||
| `sell` | `resource`, `amount` or `all=1` | resources and refined items |
|
| `sell` | `resource`, `amount` or `all=1` | resources and refined items |
|
||||||
|
|
||||||
The other sections' actions:
|
The other sections' actions:
|
||||||
@@ -186,14 +186,16 @@ short code, listed with each field, and the web's text next to it.
|
|||||||
`noSaleText` its text ("Claim its haul first").
|
`noSaleText` its text ("Claim its haul first").
|
||||||
- `bonus`: `{speed, yield}` from advisors and its attachments (capped); `gear` the
|
- `bonus`: `{speed, yield}` from advisors and its attachments (capped); `gear` the
|
||||||
attachments `[{name, image, effect}]`.
|
attachments `[{name, image, effect}]`.
|
||||||
- `drill` (with `drill=<tool id>`, else `null`): the drill panel `{toolId, slots: [{n,
|
- `drill` (with `drill=<tool id>`, once `unlocked.equipment`, else `null`): the drill panel `{toolId, slots: [{n,
|
||||||
attachment}], spares: [{name, count, cycleSecs, yield, canEquip, why, whyText, price, resale,
|
attachment}], spares: [{name, count, cycleSecs, yield, canEquip, why, whyText, price, resale,
|
||||||
confirm}], mining, resale, noSale, noSaleText, confirm, idleSame, idleBest, deployTo, ladder,
|
confirm}], mining, resale, noSale, noSaleText, confirm, idleSame, idleBest, deployTo, ladder,
|
||||||
gear}`. `attachment` is `""` for an empty slot (unequip with `item=<name>&to=<toolId>`); a
|
gear}`. `attachment` is `""` for an empty slot (unequip with `item=<name>&to=<toolId>`); a
|
||||||
spare's `cycleSecs`/`yield` are the drill's with it equipped, `why` is `fitted` (one is on
|
spare's `cycleSecs`/`yield` are the drill's with it equipped, `why` is `fitted` (one is on
|
||||||
already) or `full`; `confirm` (panel) is the sell-tool dialog, a spare's the
|
already) or `full`; `confirm` (panel) is the sell-tool dialog, a spare's the
|
||||||
sell-attachment one. `gear[].effectText` e.g. "+20% speed". While it mines, `idleSame` idle tools of its type, `idleBest` the one to deploy
|
sell-attachment one. `gear[].effectText` e.g. "+20% speed". While it mines, `idleSame` idle tools of its type, `idleBest` the one to deploy
|
||||||
and `deployTo` its best well (a suggestion, as in `buyModal`).
|
and `deployTo` its best well (a suggestion, as in `buyModal`). Its upgrade, slots, spares
|
||||||
|
and Sell drill are Store features (`upgrade`, `equip`, `unequip`, `sell-tool` are refused
|
||||||
|
before it): show them once `unlocked.store`; before that the panel is its stats and `ladder`.
|
||||||
|
|
||||||
### store (`s=store`)
|
### store (`s=store`)
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,10 @@ type DrillPanel struct {
|
|||||||
IdleBest *Tool
|
IdleBest *Tool
|
||||||
DeployTo *Suggestion
|
DeployTo *Suggestion
|
||||||
IdleURL string // opens IdleBest's panel
|
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.
|
// SlotGear is one attachment slot; Gear is nil when it's empty.
|
||||||
@@ -265,7 +269,8 @@ var noSaleText = map[string]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *State) drillPanel(row ToolRow) *DrillPanel {
|
func (s *State) drillPanel(row ToolRow) *DrillPanel {
|
||||||
d := &DrillPanel{ToolRow: row, Mining: row.Stake != nil, Resale: row.Item.ToolResale(row.Tier)}
|
d := &DrillPanel{ToolRow: row, Mining: row.Stake != nil, Resale: row.Item.ToolResale(row.Tier),
|
||||||
|
Store: s.Unlocked("store")}
|
||||||
d.NoSale = noSaleText[s.noSale(row)]
|
d.NoSale = noSaleText[s.noSale(row)]
|
||||||
for i := range cfg().Attachments.SlotsPerTool {
|
for i := range cfg().Attachments.SlotsPerTool {
|
||||||
slot := SlotGear{N: i + 1}
|
slot := SlotGear{N: i + 1}
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ type MoonGrade struct {
|
|||||||
// How many wells its moons have: weight by count, e.g. {"2": 30, "5": 8}.
|
// How many wells its moons have: weight by count, e.g. {"2": 30, "5": 8}.
|
||||||
WellCountWeights map[int]float64 `json:"wellCountWeights"`
|
WellCountWeights map[int]float64 `json:"wellCountWeights"`
|
||||||
Affinities map[string]float64 `json:"affinities,omitempty"`
|
Affinities map[string]float64 `json:"affinities,omitempty"`
|
||||||
|
// The least a new moon is worth at base prices, × its price (0: no floor). The early
|
||||||
|
// grades have one so they never lose money (no duds while players learn); a roll under
|
||||||
|
// it is rolled again (rollWells).
|
||||||
|
MinValue float64 `json:"minValue,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// grade is the moon grade with that name, else the first (lowest) one.
|
// grade is the moon grade with that name, else the first (lowest) one.
|
||||||
@@ -239,6 +243,9 @@ func (cfg *GameConfig) validateWells() error {
|
|||||||
if g.Grade == "" || cfg.gradeIndex(g.Grade) != i {
|
if g.Grade == "" || cfg.gradeIndex(g.Grade) != i {
|
||||||
return fmt.Errorf("moonGrades: grade names must be set and unique (%q)", g.Grade)
|
return fmt.Errorf("moonGrades: grade names must be set and unique (%q)", g.Grade)
|
||||||
}
|
}
|
||||||
|
if g.MinValue < 0 || g.MinValue > 3 {
|
||||||
|
return fmt.Errorf("moon grade %s: minValue must be in [0, 3] (× the price)", g.Grade)
|
||||||
|
}
|
||||||
if g.Price < 0 || g.WellSupplyMin < 0 || g.WellSupplyMin > g.WellSupplyMax {
|
if g.Price < 0 || g.WellSupplyMin < 0 || g.WellSupplyMin > g.WellSupplyMax {
|
||||||
return fmt.Errorf("moon grade %s: price and supply can't be negative, wellSupplyMin can't be above wellSupplyMax", g.Grade)
|
return fmt.Errorf("moon grade %s: price and supply can't be negative, wellSupplyMin can't be above wellSupplyMax", g.Grade)
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-13
@@ -3,15 +3,15 @@
|
|||||||
"startingResources": 0,
|
"startingResources": 0,
|
||||||
"startingTools": [1],
|
"startingTools": [1],
|
||||||
"moonGrades": [
|
"moonGrades": [
|
||||||
{"grade": "D", "price": 150, "wellSupplyMin": 16, "wellSupplyMax": 32, "wellCountWeights": {"1": 40, "2": 60},
|
{"grade": "D", "price": 300, "minValue": 1.3, "wellSupplyMin": 23, "wellSupplyMax": 46, "wellCountWeights": {"1": 40, "2": 60},
|
||||||
"affinities": {"Sollux": 6, "Shadowstone": 5, "Azurium": 1, "Novafor": 0.4, "Nebulance": 0.15}},
|
"affinities": {"Sollux": 6, "Shadowstone": 5, "Azurium": 1, "Novafor": 0.4, "Nebulance": 0.15}},
|
||||||
{"grade": "C", "price": 1500, "wellSupplyMin": 75, "wellSupplyMax": 150, "wellCountWeights": {"2": 50, "3": 50},
|
{"grade": "C", "price": 6000, "minValue": 1.3, "wellSupplyMin": 216, "wellSupplyMax": 430, "wellCountWeights": {"2": 50, "3": 50},
|
||||||
"affinities": {"Sollux": 3, "Shadowstone": 3, "Azurium": 2, "Novafor": 1, "Nebulance": 0.4}},
|
"affinities": {"Sollux": 3, "Shadowstone": 3, "Azurium": 2, "Novafor": 1, "Nebulance": 0.4}},
|
||||||
{"grade": "B", "price": 15000, "wellSupplyMin": 360, "wellSupplyMax": 960, "wellCountWeights": {"2": 30, "3": 40, "4": 30},
|
{"grade": "B", "price": 30000, "wellSupplyMin": 738, "wellSupplyMax": 1968, "wellCountWeights": {"2": 30, "3": 40, "4": 30},
|
||||||
"affinities": {"Sollux": 1, "Shadowstone": 1.5, "Azurium": 2, "Novafor": 2, "Nebulance": 1.2}},
|
"affinities": {"Sollux": 1, "Shadowstone": 1.5, "Azurium": 2, "Novafor": 2, "Nebulance": 1.2}},
|
||||||
{"grade": "A", "price": 150000, "wellSupplyMin": 2400, "wellSupplyMax": 6300, "wellCountWeights": {"3": 30, "4": 40, "5": 30},
|
{"grade": "A", "price": 300000, "wellSupplyMin": 4920, "wellSupplyMax": 12915, "wellCountWeights": {"3": 30, "4": 40, "5": 30},
|
||||||
"affinities": {"Sollux": 0.6, "Shadowstone": 1, "Azurium": 1.5, "Novafor": 2, "Nebulance": 2}},
|
"affinities": {"Sollux": 0.6, "Shadowstone": 1, "Azurium": 1.5, "Novafor": 2, "Nebulance": 2}},
|
||||||
{"grade": "S", "price": 1500000, "wellSupplyMin": 20600, "wellSupplyMax": 50600, "wellCountWeights": {"4": 50, "5": 50},
|
{"grade": "S", "price": 3000000, "wellSupplyMin": 42230, "wellSupplyMax": 103730, "wellCountWeights": {"4": 50, "5": 50},
|
||||||
"affinities": {"Sollux": 0.4, "Shadowstone": 0.6, "Azurium": 1, "Novafor": 2, "Nebulance": 3.5}}
|
"affinities": {"Sollux": 0.4, "Shadowstone": 0.6, "Azurium": 1, "Novafor": 2, "Nebulance": 3.5}}
|
||||||
],
|
],
|
||||||
"moonResale": {"factor": 0.08, "maxRatio": 0.9, "minHoldSecs": 600},
|
"moonResale": {"factor": 0.08, "maxRatio": 0.9, "minHoldSecs": 600},
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"image": "shovel.png",
|
"image": "shovel.png",
|
||||||
"price": 1200,
|
"price": 1200,
|
||||||
"claimAmount": 4,
|
"claimAmount": 4,
|
||||||
"completionTimeInSecs": 12,
|
"completionTimeInSecs": 3,
|
||||||
"upgrades": [
|
"upgrades": [
|
||||||
{"tier": 1, "price": 80, "claimBoost": 0, "speed": 1.12},
|
{"tier": 1, "price": 80, "claimBoost": 0, "speed": 1.12},
|
||||||
{"tier": 2, "price": 100, "claimBoost": 0, "speed": 1.25},
|
{"tier": 2, "price": 100, "claimBoost": 0, "speed": 1.25},
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"image": "pickaxe.png",
|
"image": "pickaxe.png",
|
||||||
"price": 5700,
|
"price": 5700,
|
||||||
"claimAmount": 20,
|
"claimAmount": 20,
|
||||||
"completionTimeInSecs": 12,
|
"completionTimeInSecs": 6,
|
||||||
"upgrades": [
|
"upgrades": [
|
||||||
{"tier": 1, "price": 400, "claimBoost": 0, "speed": 1.12},
|
{"tier": 1, "price": 400, "claimBoost": 0, "speed": 1.12},
|
||||||
{"tier": 2, "price": 470, "claimBoost": 0, "speed": 1.25},
|
{"tier": 2, "price": 470, "claimBoost": 0, "speed": 1.25},
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"image": "lunarlite.jpg",
|
"image": "lunarlite.jpg",
|
||||||
"price": 27000,
|
"price": 27000,
|
||||||
"claimAmount": 110,
|
"claimAmount": 110,
|
||||||
"completionTimeInSecs": 13,
|
"completionTimeInSecs": 8,
|
||||||
"upgrades": [
|
"upgrades": [
|
||||||
{"tier": 1, "price": 1700, "claimBoost": 0, "speed": 1.12},
|
{"tier": 1, "price": 1700, "claimBoost": 0, "speed": 1.12},
|
||||||
{"tier": 2, "price": 4500, "claimBoost": 0, "speed": 1.25},
|
{"tier": 2, "price": 4500, "claimBoost": 0, "speed": 1.25},
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"image": "lunarlite300.png",
|
"image": "lunarlite300.png",
|
||||||
"price": 290000,
|
"price": 290000,
|
||||||
"claimAmount": 1350,
|
"claimAmount": 1350,
|
||||||
"completionTimeInSecs": 14,
|
"completionTimeInSecs": 10,
|
||||||
"reach": 2,
|
"reach": 2,
|
||||||
"upgrades": [
|
"upgrades": [
|
||||||
{"tier": 1, "price": 37000, "claimBoost": 0, "speed": 1.12},
|
{"tier": 1, "price": 37000, "claimBoost": 0, "speed": 1.12},
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
"image": "regolith.jpg",
|
"image": "regolith.jpg",
|
||||||
"price": 3200000,
|
"price": 3200000,
|
||||||
"claimAmount": 17000,
|
"claimAmount": 17000,
|
||||||
"completionTimeInSecs": 15,
|
"completionTimeInSecs": 12,
|
||||||
"reach": 3,
|
"reach": 3,
|
||||||
"upgrades": [
|
"upgrades": [
|
||||||
{"tier": 1, "price": 380000, "claimBoost": 0, "speed": 1.12},
|
{"tier": 1, "price": 380000, "claimBoost": 0, "speed": 1.12},
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
"image": "dustexcavator.png",
|
"image": "dustexcavator.png",
|
||||||
"price": 7400000,
|
"price": 7400000,
|
||||||
"claimAmount": 36000,
|
"claimAmount": 36000,
|
||||||
"completionTimeInSecs": 13,
|
"completionTimeInSecs": 14,
|
||||||
"reach": 5,
|
"reach": 5,
|
||||||
"upgrades": [
|
"upgrades": [
|
||||||
{"tier": 1, "price": 470000, "claimBoost": 0, "speed": 1.12},
|
{"tier": 1, "price": 470000, "claimBoost": 0, "speed": 1.12},
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
"image": "lunarcoreextractor.png",
|
"image": "lunarcoreextractor.png",
|
||||||
"price": 11000000,
|
"price": 11000000,
|
||||||
"claimAmount": 47000,
|
"claimAmount": 47000,
|
||||||
"completionTimeInSecs": 11,
|
"completionTimeInSecs": 16,
|
||||||
"reach": 5,
|
"reach": 5,
|
||||||
"upgrades": [
|
"upgrades": [
|
||||||
{"tier": 1, "price": 840000, "claimBoost": 0, "speed": 1.12},
|
{"tier": 1, "price": 840000, "claimBoost": 0, "speed": 1.12},
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
{ "maxBatch": 2500, "price": 15000000 }
|
{ "maxBatch": 2500, "price": 15000000 }
|
||||||
],
|
],
|
||||||
"bulkLog": 1.83,
|
"bulkLog": 1.83,
|
||||||
"unlockGrade": "B",
|
"unlockGrade": "C",
|
||||||
"items": [
|
"items": [
|
||||||
{"name": "Dusk Alloy", "tier": 1, "color": "#818cf8", "price": 40, "seconds": 1,
|
{"name": "Dusk Alloy", "tier": 1, "color": "#818cf8", "price": 40, "seconds": 1,
|
||||||
"inputs": {"Sollux": 2, "Shadowstone": 2},
|
"inputs": {"Sollux": 2, "Shadowstone": 2},
|
||||||
|
|||||||
+94
-11
@@ -712,12 +712,29 @@ func createUser(ctx context.Context, db *pgxpool.Pool, name string) (string, err
|
|||||||
return userID, err
|
return userID, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// rollWells rolls the wells of a new moon of the given grade and type. The count comes from
|
// rollWells rolls the wells of a new moon of the given grade and type (rollWellsOnce). A
|
||||||
// the grade's wellCountWeights and the resources from the type's affinities times the
|
// grade with a minValue rolls again while the moon is worth less than that × its price at
|
||||||
// grade's (drawn without replacement). A player's first moon (first) always has every raw
|
// base prices (at most 64 times; then the best roll).
|
||||||
// input of some recipe. Each well's supply is rolled on its own in the grade's
|
|
||||||
// [wellSupplyMin, wellSupplyMax).
|
|
||||||
func rollWells(c *GameConfig, g *MoonGrade, kind MoonType, first bool, rnd func() float64) []wellSupply {
|
func rollWells(c *GameConfig, g *MoonGrade, kind MoonType, first bool, rnd func() float64) []wellSupply {
|
||||||
|
wells := rollWellsOnce(c, g, kind, first, rnd)
|
||||||
|
if g.MinValue <= 0 {
|
||||||
|
return wells
|
||||||
|
}
|
||||||
|
floor, best := g.MinValue*g.Price, wellsValue(c, wells)
|
||||||
|
for try := 0; best < floor && try < 64; try++ {
|
||||||
|
w := rollWellsOnce(c, g, kind, first, rnd)
|
||||||
|
if v := wellsValue(c, w); v > best {
|
||||||
|
wells, best = w, v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wells
|
||||||
|
}
|
||||||
|
|
||||||
|
// rollWellsOnce rolls one moon's wells. The count comes from the grade's wellCountWeights
|
||||||
|
// and the resources from the type's affinities times the grade's (drawn without
|
||||||
|
// replacement). A player's first moon (first) always has every raw input of some recipe.
|
||||||
|
// Each well's supply is rolled on its own in the grade's [wellSupplyMin, wellSupplyMax).
|
||||||
|
func rollWellsOnce(c *GameConfig, g *MoonGrade, kind MoonType, first bool, rnd func() float64) []wellSupply {
|
||||||
names := sortedKeys(c.ResourcePrices)
|
names := sortedKeys(c.ResourcePrices)
|
||||||
weight := func(name string) float64 {
|
weight := func(name string) float64 {
|
||||||
w := 1.0
|
w := 1.0
|
||||||
@@ -1321,7 +1338,11 @@ func claimReady(ctx context.Context, db *pgxpool.Pool, userID string, scope Clai
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// redrill puts the idle tools whose last stake was on a well back to work on it.
|
// redrill puts the idle tools whose last stake was on a well back to work on it, picked
|
||||||
|
// to fill what the well has left with the least to spare: while there is room, the
|
||||||
|
// smallest drill that would finish the job alone, else the biggest one and again (two
|
||||||
|
// 4-unit drills and a 20-unit one on a well with 3 left: one 4-unit drill goes). The rest
|
||||||
|
// stay idle.
|
||||||
func redrill(ctx context.Context, db *pgxpool.Pool, userID, wellID string) (int, error) {
|
func redrill(ctx context.Context, db *pgxpool.Pool, userID, wellID string) (int, error) {
|
||||||
if !uuidPattern.MatchString(wellID) {
|
if !uuidPattern.MatchString(wellID) {
|
||||||
return 0, gameErr("Well not found")
|
return 0, gameErr("Well not found")
|
||||||
@@ -1329,7 +1350,8 @@ 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 from inventory_item
|
select inventory_item.id, store_item.claim_amount 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
|
||||||
@@ -1342,17 +1364,41 @@ func redrill(ctx context.Context, db *pgxpool.Pool, userID, wellID string) (int,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tools, err := pgx.CollectRows(rows, pgx.RowTo[string])
|
type idle struct {
|
||||||
|
ID string
|
||||||
|
Amount float64
|
||||||
|
}
|
||||||
|
tools, err := pgx.CollectRows(rows, pgx.RowToStructByPos[idle])
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
var moonID string
|
||||||
|
if err := tx.QueryRow(ctx, "select source_id from resource_well where id = $1", wellID).Scan(&moonID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// room is what the well has left after the drills already on it (and the ones
|
||||||
|
// just sent) take their share.
|
||||||
|
room := func() (float64, error) {
|
||||||
|
m, stakes, err := moonInTx(ctx, tx, userID, moonID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return newStakeTake(m, stakes, wellID, 1e18, 1), nil
|
||||||
|
}
|
||||||
n = 0
|
n = 0
|
||||||
// Until the well has no room left: the rest stay idle (none fits: the error).
|
for len(tools) > 0 {
|
||||||
for _, id := range tools {
|
left, err := room()
|
||||||
err := stakeInTx(ctx, tx, userID, id, wellID)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if left <= 1e-9 {
|
||||||
|
// This well is full: drills that mine several wells may still have work on
|
||||||
|
// the moon (stakeInTx decides), in order, as before.
|
||||||
|
for _, t := range tools {
|
||||||
|
err := stakeInTx(ctx, tx, userID, t.ID, wellID)
|
||||||
if (err == errWellCommitted || err == errWellEmpty) && n > 0 {
|
if (err == errWellCommitted || err == errWellEmpty) && n > 0 {
|
||||||
break
|
break
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@@ -1360,11 +1406,48 @@ func redrill(ctx context.Context, db *pgxpool.Pool, userID, wellID string) (int,
|
|||||||
}
|
}
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
amounts := make([]float64, len(tools))
|
||||||
|
for i, t := range tools {
|
||||||
|
amounts[i] = t.Amount
|
||||||
|
}
|
||||||
|
pick := redrillPick(amounts, left)
|
||||||
|
err = stakeInTx(ctx, tx, userID, tools[pick].ID, wellID)
|
||||||
|
if (err == errWellCommitted || err == errWellEmpty) && n > 0 {
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tools = slices.Delete(tools, pick, pick+1)
|
||||||
|
n++
|
||||||
|
}
|
||||||
return logEvent(ctx, tx, userID, "redrill", map[string]any{"well": wellID, "drills": n})
|
return logEvent(ctx, tx, userID, "redrill", map[string]any{"well": wellID, "drills": n})
|
||||||
})
|
})
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redrillPick is the drill (index into amounts, what each takes a claim) to send to a
|
||||||
|
// well with left to fill: the smallest that covers it alone, else the biggest.
|
||||||
|
func redrillPick(amounts []float64, left float64) int {
|
||||||
|
pick := -1
|
||||||
|
for i, a := range amounts {
|
||||||
|
if a >= left-1e-9 && (pick < 0 || a < amounts[pick]) {
|
||||||
|
pick = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pick >= 0 {
|
||||||
|
return pick
|
||||||
|
}
|
||||||
|
pick = 0
|
||||||
|
for i, a := range amounts {
|
||||||
|
if a > amounts[pick] {
|
||||||
|
pick = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pick
|
||||||
|
}
|
||||||
|
|
||||||
// coverWells picks the wells (indexes into supply) a stake with the given reach mines:
|
// coverWells picks the wells (indexes into supply) a stake with the given reach mines:
|
||||||
// the one it was deployed on (anchor; -1 for none) first, then the others richest first,
|
// the one it was deployed on (anchor; -1 for none) first, then the others richest first,
|
||||||
// leaving out empty ones, up to reach wells.
|
// leaving out empty ones, up to reach wells.
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ package main
|
|||||||
// - 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 and the
|
// Help, Leaderboard, moon sale, "Claim and redeploy", upgrades, attachments and selling
|
||||||
// drill panel (attachments); Claim all also needs 2 tools.
|
// tools (the drill panel opens with Equipment and shows these parts once the Store is
|
||||||
|
// unlocked); Claim all also needs 2 tools.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Redrill sends the smallest drill that finishes the well alone, else the biggest and again.
|
||||||
|
func TestRedrillPick(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
amounts []float64
|
||||||
|
left float64
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{[]float64{4, 4, 20}, 3, 0}, // a 4 covers 3: not the 20
|
||||||
|
{[]float64{20, 4, 4}, 4, 1}, // exactly 4
|
||||||
|
{[]float64{4, 20, 4}, 10, 1}, // only the 20 covers 10
|
||||||
|
{[]float64{4, 20, 4}, 30, 1}, // none covers 30: the biggest first
|
||||||
|
{[]float64{4, 4}, 6, 0}, // none covers 6: a 4, then the other
|
||||||
|
{[]float64{110, 20, 4}, 19, 1}, // the 20, not the 110
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := redrillPick(c.amounts, c.left); got != c.want {
|
||||||
|
t.Errorf("redrillPick(%v, %v) = %d, want %d", c.amounts, c.left, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,9 @@ func TestTemplates(t *testing.T) {
|
|||||||
|
|
||||||
unlocked := newPlayerState() // everything open but no moon yet
|
unlocked := newPlayerState() // everything open but no moon yet
|
||||||
unlocked.Onboarding = []string{"all"}
|
unlocked.Onboarding = []string{"all"}
|
||||||
for _, s := range []*State{veteranState(), newPlayerState(), unlocked, fullBoardState()} {
|
early := veteranState() // Equipment open, the Store not yet (a drill panel without its Store parts)
|
||||||
|
early.Onboarding = []string{"equipment", "market"}
|
||||||
|
for _, s := range []*State{veteranState(), newPlayerState(), unlocked, fullBoardState(), early} {
|
||||||
for _, v := range testViews(s) {
|
for _, v := range testViews(s) {
|
||||||
s.pagePastInMemory(v.Page)
|
s.pagePastInMemory(v.Page)
|
||||||
v = v.resolve(s)
|
v = v.resolve(s)
|
||||||
|
|||||||
+3
-1
@@ -206,7 +206,9 @@ func (v View) resolve(s *State) View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if v.Section != "equipment" || s.tool(v.Drill) == nil || !s.Unlocked("store") {
|
// A tool's panel opens with Equipment; what in it belongs to the Store (upgrade,
|
||||||
|
// attachments, selling) shows once the Store is unlocked (DrillPanel.Store).
|
||||||
|
if v.Section != "equipment" || s.tool(v.Drill) == nil || !s.Unlocked("equipment") {
|
||||||
v.Drill = ""
|
v.Drill = ""
|
||||||
}
|
}
|
||||||
v.Drill = s.idleCopy(v.Drill)
|
v.Drill = s.idleCopy(v.Drill)
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A tool's panel opens as soon as Equipment is unlocked; its Store parts (upgrade,
|
||||||
|
// attachments, selling) show only once the Store is.
|
||||||
|
func TestViewDrillPanelOnboarding(t *testing.T) {
|
||||||
|
tmpl, err := (&server{}).parseTemplates()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse: %v", err)
|
||||||
|
}
|
||||||
|
for _, c := range []struct {
|
||||||
|
flags []string
|
||||||
|
open, store bool
|
||||||
|
}{
|
||||||
|
{nil, false, false},
|
||||||
|
{[]string{"equipment"}, true, false},
|
||||||
|
{[]string{"equipment", "market"}, true, false},
|
||||||
|
{[]string{"equipment", "market", "store"}, true, true},
|
||||||
|
{[]string{"all"}, true, true},
|
||||||
|
} {
|
||||||
|
s := veteranState()
|
||||||
|
s.Onboarding = c.flags
|
||||||
|
id := s.Tools[0].ID
|
||||||
|
v := View{Section: "equipment", Drill: id}.resolve(s)
|
||||||
|
if (v.Drill != "") != c.open {
|
||||||
|
t.Errorf("%v: drill %q, want open %v", c.flags, v.Drill, c.open)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !c.open {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p := s.page(v)
|
||||||
|
if p.Drill == nil || p.Drill.Store != c.store {
|
||||||
|
t.Errorf("%v: panel %+v, want store %v", c.flags, p.Drill, c.store)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var b bytes.Buffer
|
||||||
|
if err := tmpl.ExecuteTemplate(&b, "drill-panel", p.Drill); err != nil {
|
||||||
|
t.Fatalf("render: %v", err)
|
||||||
|
}
|
||||||
|
html := b.String()
|
||||||
|
if !strings.Contains(html, "<h3>Tiers</h3>") {
|
||||||
|
t.Errorf("%v: no tier ladder", c.flags)
|
||||||
|
}
|
||||||
|
for _, part := range []string{"/act/sell-tool", "<h3>Slots</h3>", "Spare attachments"} {
|
||||||
|
if strings.Contains(html, part) != c.store {
|
||||||
|
t.Errorf("%v: %q shown %v, want %v", c.flags, part, !c.store, c.store)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -364,13 +364,9 @@
|
|||||||
{{$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}}">
|
||||||
{{if $open}}
|
<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}}: attachments"><img src="/static/assets/items/{{.Item.Image}}" alt="" onerror="this.style.visibility='hidden'"></a>
|
|
||||||
{{else}}
|
|
||||||
<div class="card-media"><img src="/static/assets/items/{{.Item.Image}}" alt="" onerror="this.style.visibility='hidden'"></div>
|
|
||||||
{{end}}
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2>{{if $open}}<a href="{{$d}}" hx-get="{{$d}}" class="row-link">{{.Item.Name}}</a>{{else}}{{.Item.Name}}{{end}}{{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}} <span class="tier-chip num" title="Tier">T{{.Tier}}/{{.MaxTier}}</span></h2>
|
||||||
<p class="tool-status small">
|
<p class="tool-status small">
|
||||||
{{with .Stake}}
|
{{with .Stake}}
|
||||||
{{$url := $.StakeURL .}}
|
{{$url := $.StakeURL .}}
|
||||||
@@ -810,8 +806,11 @@
|
|||||||
{{define "drill-panel"}}
|
{{define "drill-panel"}}
|
||||||
<h2>{{.Item.Name}} <span class="muted">T{{.Tier}}</span></h2>
|
<h2>{{.Item.Name}} <span class="muted">T{{.Tier}}</span></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). */}}
|
||||||
|
{{if .Store}}
|
||||||
{{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}}
|
{{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}}
|
||||||
<div class="drill-upgrade">{{template "tool-upgrade" .ToolRow}}</div>
|
<div class="drill-upgrade">{{template "tool-upgrade" .ToolRow}}</div>
|
||||||
|
{{end}}
|
||||||
{{with .Ladder}}
|
{{with .Ladder}}
|
||||||
<h3>Tiers</h3>
|
<h3>Tiers</h3>
|
||||||
<ol class="tier-ladder num small">
|
<ol class="tier-ladder num small">
|
||||||
@@ -823,7 +822,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</ol>
|
</ol>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Mining}}<p class="muted small">Mining now. Changes apply from its next drill.</p>{{end}}
|
{{if .Mining}}<p class="muted small">Mining now.{{if .Store}} Changes apply from its next drill.{{end}}</p>{{end}}
|
||||||
{{with .IdleBest}}
|
{{with .IdleBest}}
|
||||||
{{$b := $.IdleURL}}
|
{{$b := $.IdleURL}}
|
||||||
<div class="line">
|
<div class="line">
|
||||||
@@ -836,6 +835,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Store}}
|
||||||
<h3>Slots</h3>
|
<h3>Slots</h3>
|
||||||
{{range .Slots}}
|
{{range .Slots}}
|
||||||
<div class="line">
|
<div class="line">
|
||||||
@@ -871,6 +871,7 @@
|
|||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{/* Claim button for a moon's multi-well drills (claim-wide), a DrillGroup. */}}
|
{{/* Claim button for a moon's multi-well drills (claim-wide), a DrillGroup. */}}
|
||||||
|
|||||||
+10
-4
@@ -79,11 +79,17 @@ func TestWellsDistribution(t *testing.T) {
|
|||||||
mean := all / float64(total) / price
|
mean := all / float64(total) / price
|
||||||
fmt.Printf("all moons: mean value %.0f (%.2f× the price), %.0f units, %.1f wells\n",
|
fmt.Printf("all moons: mean value %.0f (%.2f× the price), %.0f units, %.1f wells\n",
|
||||||
all/float64(total), mean, units/float64(total), float64(wellsRolled)/float64(total))
|
all/float64(total), mean, units/float64(total), float64(wellsRolled)/float64(total))
|
||||||
// A moon is worth about 1.3–1.8× its price at base prices (BALANCE-NOTES.md).
|
// A moon is worth about 1.3–1.8× its price at base prices (BALANCE-NOTES.md); a grade
|
||||||
if mean < 1.3 || mean > 1.8 {
|
// with a minValue (no duds early on) may average up to 2×.
|
||||||
t.Errorf("grade %s: moons are worth %.2f× their price on average, want 1.3–1.8×", g.Grade, mean)
|
hi := 1.8
|
||||||
|
if g.MinValue > 0 {
|
||||||
|
hi = 2.0
|
||||||
}
|
}
|
||||||
if perWell := units / float64(wellsRolled); math.Abs(perWell/(mid-0.5)-1) > 0.02 {
|
if mean < 1.3 || mean > hi {
|
||||||
|
t.Errorf("grade %s: moons are worth %.2f× their price on average, want 1.3–%.1f×", g.Grade, mean, hi)
|
||||||
|
}
|
||||||
|
// (a minValue re-rolls the poorest moons: their wells come out somewhat bigger)
|
||||||
|
if perWell := units / float64(wellsRolled); math.Abs(perWell/(mid-0.5)-1) > 0.02 && (g.MinValue == 0 || perWell < mid-0.5 || perWell > (mid-0.5)*1.1) {
|
||||||
t.Errorf("grade %s: mean well %.1f units, expected about %.1f", g.Grade, perWell, mid-0.5)
|
t.Errorf("grade %s: mean well %.1f units, expected about %.1f", g.Grade, perWell, mid-0.5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user