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