Compare commits
2
Commits
80f874c452
...
f0d396c2eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0d396c2eb | ||
|
|
09aa0978bc |
No files matched your search
+1
-11
@@ -1,8 +1,3 @@
|
||||
# Build output
|
||||
/bin/
|
||||
/moonminers
|
||||
/web/static/css/app.css
|
||||
|
||||
# Local env files (never commit)
|
||||
.env
|
||||
.env*.local
|
||||
@@ -12,14 +7,9 @@
|
||||
.DS_Store
|
||||
|
||||
# Personal files
|
||||
/sql/create-users.sql
|
||||
/.dir-locals.el
|
||||
sql/queries.sql
|
||||
/project.todo
|
||||
|
||||
# Blender files (generated by art/equipment scripts)
|
||||
# Blender files
|
||||
*.blend
|
||||
*.blend1
|
||||
/art/equipment/build/
|
||||
/art/advisors/build/
|
||||
/art/attachments/build/
|
||||
@@ -1,195 +1,41 @@
|
||||
# AGENTS.md — Moon Miners
|
||||
|
||||
## What this is
|
||||
Moon Miners is a browser idle/mining game. Players join anonymously: the server creates a
|
||||
player and keeps its id in the `mm_user` cookie. Players own moons ("staking sources") with
|
||||
resource wells, buy and upgrade mining tools (store items) and stake them on wells. When a
|
||||
stake finishes (drill cycles are seconds: `completionTimeInSecs`, `duration_in_secs`) they
|
||||
claim resources (Sollux, Shadowstone, Azurium, Novafor, Nebulance), which
|
||||
they can sell for credits (₥) or refine into refined items (Refinery section: timed batches
|
||||
in slots; refined items sell for more and are parts for higher tool upgrades). Drill
|
||||
attachments (bought in the Store, equipped on a drill from its panel in Equipment) give it
|
||||
a modifier that makes it faster or bigger; advisors (Advisors section, one per seat) boost drill speed, yield, sell prices or the
|
||||
refinery for the whole account. There is also a leaderboard (Leaderboard section, `leaderboard.go`: top miners by credits, banned players left out; also JSON at `/api/leaderboard`).
|
||||
Moon Miners is an idle/mining game. Players join anonymously: the server creates a
|
||||
player and keeps its id in the `mm_user` cookie (or `X-MM-User` for the API). Players own
|
||||
moons ("staking sources") with resource wells, buy and upgrade mining tools (store items)
|
||||
and stake them on wells. When a stake finishes (drill cycles are seconds:
|
||||
`completionTimeInSecs`, `duration_in_secs`) they claim resources (Sollux, Shadowstone,
|
||||
Azurium, Novafor, Nebulance), which they can sell for credits (₥) or refine into refined
|
||||
items (Refinery section: timed batches in slots; refined items sell for more and are parts
|
||||
for higher tool upgrades). Drill attachments (bought in the Store, equipped on a drill from
|
||||
its panel in Equipment) give it a modifier that makes it faster or bigger; advisors
|
||||
(Advisors section, one per seat) boost drill speed, yield, sell prices or the refinery for
|
||||
the whole account. There is also a leaderboard (Leaderboard section: top miners by credits,
|
||||
banned players left out).
|
||||
|
||||
## Stack
|
||||
- Go (standard library `net/http` + `html/template`) with `pgx` for Postgres. One `main`
|
||||
package at the repo root; templates, static files and SQL are embedded with `go:embed`.
|
||||
- The interface is server-rendered HTML. htmx swaps it in place, morphing the game's regions
|
||||
(idiomorph extension, vendored in `web/static/vendor/idiomorph`) so elements keep their
|
||||
state; Tailwind v4 (standalone CLI) builds the CSS. No bundler, no TypeScript, no Alpine.
|
||||
- three.js is a plain ES module loaded through an import map (vendored in `web/static/vendor`).
|
||||
The viewport in the aside shows the selected moon, generated procedurally from its id
|
||||
(`terrain.js`, run in a worker by `moon-worker.js`; settings tuned on `/utils/moons` and
|
||||
stored in `moon_settings`). The HTML is the interface; the moon is only decoration.
|
||||
- Postgres 16, stock image. No functions or extensions in the database.
|
||||
## How the parts fit
|
||||
- `server/`: the Go server (game rules, Postgres, admin pages) and the web client, which it
|
||||
renders as HTML (htmx, three.js moon). The server is the only authority on game state.
|
||||
Details: `server/AGENTS.md`.
|
||||
- `client/`: the Defold client (desktop and a WebAssembly build). It talks to the server's
|
||||
JSON API (`/api/state`, `/api/act/<action>`, documented in `server/api.md`) and shows
|
||||
what it sends; its moon generator is a port of `server/web/static/js/terrain.js`.
|
||||
Details: `client/AGENTS.md`.
|
||||
- Repo root: `docker-compose.yml` (local Postgres), `docker-compose.prod.yml` (production,
|
||||
Dokploy; builds `server/`), `IDEAS.md` (features not built yet), `README.md`.
|
||||
|
||||
## Layout
|
||||
- The repo is `game/` in the `MoonMiners/` workspace; agent worktrees go next to it
|
||||
(`git worktree add ../wt-<task> -b <task>`). The Defold desktop client is `client/` (its own
|
||||
AGENTS.md).
|
||||
- `main.go`: commands (`serve`, `migrate`, `load-config`, `sim [file]`, `moon-traits`), `.env`
|
||||
loading. `balance.go`: the `sim` command (prints the `sim.go` analysis).
|
||||
- `game.go`: game rules. Each action is one transaction that locks (`for update`) the rows it
|
||||
reads; `check (balance >= 0)` in the schema is the safety net. Rule violations return a
|
||||
`GameError` (shown to the player as a toast); other errors are 500s. Past moons (wells
|
||||
empty, nothing unclaimed) are computed (`loadPastMoons`, no column); they still count as
|
||||
owned for the moon price and well scale, and can't be sold. The Past tab loads them 10 at a
|
||||
time (`loadPastPage`, limit/offset; `page=` in the view URL); handlers load state with
|
||||
`loadView` (state, the past page, the resolved view).
|
||||
- `views.go`: the `View` (section, moon, well, tab; carried in the URL as
|
||||
`/?s=&moon=&well=`), the `Page` view model, formatting helpers.
|
||||
- `server.go`: routes and middleware (request log, `nosniff`/`Referrer-Policy` headers, body
|
||||
limits with forms parsed up front, a per-IP limit on new players). `GET /` renders the page
|
||||
(htmx requests get every region as an out-of-band swap); `POST /act/<action>` runs an
|
||||
action and responds the same way.
|
||||
- `api.go`, `api.md`: JSON API for non-web clients (Defold): `GET /api/state` (the view
|
||||
models mapped to JSON DTOs; times in unix ms) and `POST /api/act/<action>` (same
|
||||
`runAction` as `/act/`, gates included; `{ok, error, toast, fx, state}`, rule errors 422).
|
||||
Player id from `X-MM-User` or the cookie, else a new player (id echoed back).
|
||||
`api_sections.go`: every section's block (equipment and drill panel, store, refinery,
|
||||
advisors, leaderboard, help, new-moon teaser, reveal and buy modal in act responses, moon
|
||||
sale, market chart), plus `rules` (numbers for client prediction), `catalog` (static data,
|
||||
image URLs, tool codes), `jobs`, `bonuses`, `liveEvents`; blocks come with `s=` or
|
||||
`include=`; reasons are codes with the web's text beside them (`whyText`, …), confirm dialogs as `confirm`; `catalog=0` or `catalog=<catalogVersion>` skips the catalog; `clientSeq` is echoed. A new section in the API:
|
||||
map its view model in `addSection`. `api_test.go` (in `make check`) builds every block
|
||||
for every test view.
|
||||
- `config/game.json`: the game config (starting credits/resources/tools, moon price, well
|
||||
supply, `wellCountWeights`, resource prices, store items with upgrade tiers, moon types: a
|
||||
name, a placeholder description and resource `affinities`, refinery), embedded.
|
||||
`rollWells` (game.go) rolls a new moon's wells: 2–5 by weight, resources by its type's
|
||||
affinities, each well's supply rolled in [`wellSupplyMin`, `wellSupplyMax`) (fewer wells
|
||||
hold less); a first moon
|
||||
always has every raw input of a recipe. A moon's name comes from `moonNames` (roots and
|
||||
suffix style weights, `rollMoonName`), never repeated among a player's moons; its type is
|
||||
`staking_source.kind` (moons from before migration 013 were named after their type).
|
||||
`config.go` parses it into `game` and syncs store items into `store_item`/`upgrade_item`
|
||||
(needed for the inventory foreign key and the SQL in game.go). Admins override its
|
||||
values live on `/utils/economy` (table editor in `economy.js`; `economy.go`: a patch per
|
||||
save in `config_override`, latest wins; `sim.go`: balance analysis and progression sim). Read config values through
|
||||
`cfg()` (game.json + override), never `game` directly. `migrate.go`: database setup.
|
||||
- `db.go`: connections, `inTx`, `GameError`.
|
||||
- `market.go`: market prices. A unit sells for its config price × a deterministic drift
|
||||
(name + 5-minute tick, within ±`marketVolatility`) × live price events × the player's
|
||||
selling pressure (`marketPressure`: their recent sales of the item, in base-price credits,
|
||||
decaying with a half-life, table `market_pressure`; the factor eases from 1 towards
|
||||
`floor`, and a sale is priced at its average over the pressure it adds). `marketRate` is
|
||||
used by `sell`/`sellRefined` and the market view (24h history recomputed, no table);
|
||||
view model for the Market section (ticker, watchlist, chart, sell ticket; `pick=` in the
|
||||
URL selects a quote). `web/static/js/market.js`: chart crosshair and ticket estimate.
|
||||
- `resale.go`: selling a moon back (`moonResale` in game.json): what's left in its wells at
|
||||
market price (with live events, without selling pressure or advisors) × `factor`, capped
|
||||
at `maxRatio` × the replacement moon's price (`moonPriceFor(owned-1)`), which it's
|
||||
compared with. Not the last moon, not within `minHoldSecs` of buying it, not while a
|
||||
drill on it is running; finished drills are claimed in the sale. Template: `moon-sale`.
|
||||
Also selling tools and spare attachments for `equipmentResale.factor` (below 1) of their
|
||||
credit price (a tool: price + its upgrades; refined parts not refunded): only idle
|
||||
tools with nothing to claim, never the last tool; attachments on it return to the inventory.
|
||||
- `refinery.go`: refinery config (`refinery` in game.json, `parts` on upgrade tiers), rules
|
||||
(start batch, collect, buy slot, sell refined, upgrade parts) and its view model. A batch
|
||||
of n takes `seconds` × n^`bulkExponent` (`batchSecs`). Tables
|
||||
`refined_account`, `refinery_job`, `users.refinery_slots`. Template: `refinery.html`.
|
||||
- `bonus.go`: player bonuses. `bonusesFor` combines advisors and a tool's attachments
|
||||
(multipliers multiply, then each kind is capped by `bonusCaps`); live events multiply on
|
||||
top, uncapped. `drillStats` turns a tool's cycle and yield into a stake's (used by
|
||||
`stakeInTx`, the views and the sim); sells and refinery batches use `Bonus.Sell`/`Refine`.
|
||||
Bonuses are fixed when the action runs (a stake keeps its duration and amount).
|
||||
- `attachments.go`: attachments config (`attachments` in game.json: `slotsPerTool`, items
|
||||
with `speed`/`yield` multipliers), buy/equip/unequip rules (one of each per tool; equip
|
||||
and unequip any time, they count from the next stake), store cards and the drill panel
|
||||
(`drill=` in the URL, Equipment section, in the aside). Table `attachment`
|
||||
(`inventory_item_id` null: unequipped).
|
||||
- `advisors.go`, `web/templates/advisors.html`: advisors config (`advisors`), hire, buy
|
||||
seat, seat/unseat/swap/dismiss rules (only seated advisors give a bonus; dismissing
|
||||
refunds nothing), Advisors section and the advisor panel (`adv=` in the URL, in the
|
||||
aside). Tables `advisor_hire` (`seat`, null: not seated), `users.advisor_seats`.
|
||||
- `onboarding.go`: onboarding by progressive reveal, no tutorial text. `users.onboarding`
|
||||
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
|
||||
reveals Equipment, claiming Market, selling Store (and "+ New moon", Help, upgrades,
|
||||
attachments, moon sale, Redrill), claiming both raw inputs of a recipe Refinery, buying a
|
||||
tool or attachment Advisors. Templates ask `.S.Unlocked "<feature>"`; `View.resolve` sends
|
||||
locked sections to the moon view and `runAction` refuses locked actions (`actionGates`).
|
||||
A revealed tab shines (`data-shine`) until opened (`markSeen` in `handleIndex`). New
|
||||
sections and actions need a gate there.
|
||||
- `players.go`, `web/templates/players.html`: admin Players page at `/utils/players`
|
||||
(search, detail, grants, finish drills, reset, onboarding reset/unlock all, ban; each logged as `admin_*` in
|
||||
`event_log`). `notBanned` gives banned players (`users.banned_at`) a suspended page.
|
||||
`web/templates/admin.html`: the shared admin nav (`{{template "admin-nav" "<page>"}}`, put
|
||||
it on every `/utils` page) and confirm dialog. `/utils` redirects to `/utils/players`.
|
||||
- `web/templates/`: `page.html` (page shell, boot page), `app.html` (header, moon
|
||||
list, sections, side panel).
|
||||
- `web/static/js/main.js`: glue between htmx and the scene (and what a morph keeps of the JS's
|
||||
own changes: classes, styles, inserted elements, typed values), and countdowns (`data-ends`)
|
||||
that enable their Claim button (`data-ready-at`) when due. `scene.js`: the three.js scene.
|
||||
`tabs.js`: sliding tab highlight (`data-tabs`). `enter.js`: entrance animations on view
|
||||
changes. `ui.js`: the custom confirm dialog (`hx-confirm`) and toasts, shared by the game
|
||||
and admin pages.
|
||||
- `web/static/js/fx.js`: effects per action result (toast `data-fx`). `sfx.js`: sound
|
||||
(Web Audio; clips in `web/static/assets/sfx`, see CREDITS.txt; synth sounds via vendored
|
||||
ZzFX). fx.js maps action kinds to sounds; mute is in the header, `window.mmAudio` for
|
||||
outside control (e.g. ads).
|
||||
`terrain.js`: moon generation (no imports, also runs in the worker). `moons-admin.js` and
|
||||
`web/templates/moons.html`: the moon lab. `moons.go`: moon settings endpoints, `isAdmin`.
|
||||
`moondesc.go`: moon descriptions generated from the look (mirrors terrain.js `moonTraits`;
|
||||
`scripts/check-moon-traits.mjs` in `make check` keeps them in sync) and the wells.
|
||||
- `reveal.go`, `web/static/js/reveal.js`: the new-moon section (`s=new-moon`, a teaser with a
|
||||
shrouded moon in the viewport, `mystery` in the scene data) and the reveal after
|
||||
`buy-moon` (the response carries `#reveal` with the moon's card; scene.js plays the
|
||||
burst, fx.js waits for `mm:revealed` before counting the balance down).
|
||||
`rating.go`: a new moon's rating, the percentile of its base-price value among a cached
|
||||
sample of `rollWells` (resampled when the config changes), stored in
|
||||
`staking_source.rating`; tiers Poor/Fair/Good/Great/Exceptional drive the card's verdict
|
||||
and the star chip in the moon list and header.
|
||||
- `liveops.go`, `events.go`, `web/templates/liveops.html`, `web/static/js/liveops.js`: live
|
||||
ops at `/utils/liveops` (dashboard from `event_log`, timed events in `live_event` applied
|
||||
via `eventMultiplier` in game.go, `announcement`; both shown in the game's `#banner`).
|
||||
- `web/css/app.css`: Tailwind input (custom HUD styles); builds to `web/static/css/app.css`.
|
||||
- `sql/tables.sql`, `sql/data.sql`: schema for fresh databases. `sql/migrations/` (001–016):
|
||||
changes for existing ones, applied on every `migrate`, so each must be idempotent.
|
||||
- `test-endpoints.restclient`: the up-to-date list of endpoints, in Emacs restclient format.
|
||||
## Workspace
|
||||
- The repo is `game/` in the `MoonMiners/` workspace. Design notes live in the workspace,
|
||||
outside git (`../project.todo`, `../FAQ.org`, `../druid-notes.md`).
|
||||
- Agent worktrees go in the workspace, next to `game/`:
|
||||
`git -C game worktree add ../wt-<task> -b <task>`. Remove them when merged.
|
||||
- `.env` (database credentials) is in the repo root or in `server/`. Never read, print or
|
||||
commit it.
|
||||
|
||||
## Commands
|
||||
- `make dev`: server on :3000 that reloads templates/static from disk, with Tailwind watching.
|
||||
- `make build`, `make check` (gofmt, vet, template check, JS syntax of every `web/static/js/*.js`,
|
||||
moon traits check), `make migrate`, `make load-config`. `go run . sim` prints the economy analysis.
|
||||
- Template check: `templates_test.go` (`go test -run Templates`, in `make check`) parses the
|
||||
templates like the server and renders every page and region against in-memory game states;
|
||||
when you add a page, section or view model branch, make sure its states/views reach it.
|
||||
- Local DB: `podman-compose up -d db` then `make migrate` (see README).
|
||||
There is no other test suite, except `wells_test.go` (`go test -run Wells -v`: well roll
|
||||
distribution, value by well count, resale odds, sim seeds).
|
||||
|
||||
## Deployment
|
||||
- Production uses `docker-compose.prod.yml` (deployed with Dokploy): `app`, `db` (stock
|
||||
Postgres) and `migrate` (same image as `app`, runs on every deploy before the app starts).
|
||||
- Schema changes need both an update to `sql/tables.sql` (fresh DBs) and a new idempotent
|
||||
`sql/migrations/NNN-name.sql` (existing DBs). Check that a fresh DB and one built from the
|
||||
pre-Go schema (`git show 610d26e:sql/tables.sql`) plus all migrations give the same
|
||||
`pg_dump --schema-only`.
|
||||
- Keep `docker-compose.prod.yml` services on the explicit `internal` network (Dokploy adds its own network).
|
||||
|
||||
## Environment
|
||||
Postgres credentials come from `.env`: `DB_USER`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`,
|
||||
`DB_DATABASE`, plus `ADMIN_DB_USER`/`ADMIN_DB_PASSWORD` for `migrate`. Never read, print or
|
||||
commit `.env`. `ADMIN_USER_IDS` (comma-separated player ids) grants access to `/utils`
|
||||
(unset: nobody); with `MM_DEV=1` everyone is an admin.
|
||||
|
||||
## Conventions
|
||||
- Game rules go in `game.go`, in a transaction, locking what they read. Keep handlers thin.
|
||||
- Layout: header, moon list, main area (one section at a time), and an aside with the 3D
|
||||
viewport and the selected well. Links are `<a href hx-get>` to a view URL; main.js keeps
|
||||
the address bar in sync.
|
||||
- New section: add it to `sections` and `State.page` (views.go) and a `<name>-section`
|
||||
template in `app.html`. New action: a case in `server.runAction` (shared by `/act/` and
|
||||
`/api/act/`) plus a button with `hx-post="/act/<action>"`. The current view (s, moon, well, drill) is sent automatically,
|
||||
so action parameters must use other names. Give it a sound in fx.js if it needs one.
|
||||
- Clicks give instant feedback only (busy state + spinner). No client-side game logic or
|
||||
prediction; the server is the authority. Pages don't reload on timers: countdown labels
|
||||
(`data-ends`) tick client-side and may enable a Claim button, as display only.
|
||||
- SQL is lowercase with snake_case identifiers.
|
||||
- When you add an endpoint, add it to `test-endpoints.restclient`.
|
||||
- New player action: log it with `logEvent(ctx, tx, userID, "<kind>", data)` inside its
|
||||
transaction (spends use the key `price`, earnings `credits`; the dashboard sums them).
|
||||
- Dispose any three.js geometry and materials you create.
|
||||
## Conventions (all parts)
|
||||
- The server is the authority: clients show the state it sends and send actions; no game
|
||||
rules on the client side beyond what `server/api.md` "Prediction" allows.
|
||||
- Work in one part at a time: a client task doesn't change `server/`, and the other way
|
||||
round, unless the task says so.
|
||||
- Keep changes small. Don't refactor or move files unless asked.
|
||||
@@ -1,252 +1,47 @@
|
||||
# Moon Miners
|
||||
|
||||
A browser mining game. Players own moons (staking sources) with resource wells, buy and
|
||||
upgrade mining tools, stake them on wells, claim resources when the stake completes, and sell
|
||||
resources for credits (₥). Drill cycles last seconds, so play is active. The refinery turns
|
||||
resources into refined items on a timer; those sell for more and are needed as parts for
|
||||
higher tool upgrades.
|
||||
An idle mining game. Players own moons with resource wells, buy and upgrade mining tools,
|
||||
stake them on wells, claim resources when the drills finish, and sell or refine them for
|
||||
credits (₥). Drill cycles last seconds, so play is active. Players join anonymously; no
|
||||
wallet or sign-up.
|
||||
|
||||
It's a Go server that renders the interface as HTML (with htmx for in-place updates) next to
|
||||
a three.js view of the selected moon, generated procedurally from its id. Game rules run in
|
||||
Go, one Postgres transaction per action. The page doesn't reload on timers: countdowns tick
|
||||
in the browser and enable each Claim button when its drill should be done, and the server
|
||||
decides what is actually ready when it's clicked.
|
||||
| Directory | What it is |
|
||||
|---|---|
|
||||
| `server/` | The Go server (game rules in Postgres transactions, admin pages, JSON API) and the web client it renders as HTML (htmx, a three.js moon). See `server/README.md`. |
|
||||
| `client/` | The Defold client (desktop, and a WebAssembly build), talking to the server's JSON API (`server/api.md`). See `client/AGENTS.md`. |
|
||||
| `docker-compose.yml` | Local Postgres for development. |
|
||||
| `docker-compose.prod.yml` | Production stack (Dokploy): `db`, `migrate`, `app`, built from `server/`. |
|
||||
|
||||
Players join anonymously: on the first visit the server creates a player and stores its id in
|
||||
a cookie. No wallet or sign-up needed.
|
||||
## Run the server
|
||||
|
||||
## Requirements
|
||||
Needs Go 1.25+, Podman or Docker (for Postgres), and Node.js for `make check`.
|
||||
|
||||
- Go 1.25+
|
||||
- PostgreSQL 16 (stock image is fine), or Podman/Docker to run it
|
||||
- Node.js, only for `make check` (syntax-checks the JS)
|
||||
|
||||
`make` downloads the Tailwind standalone CLI into `bin/` on first use.
|
||||
|
||||
## Environment
|
||||
|
||||
The server reads `.env` in the project root (gitignored, never commit it). Real environment
|
||||
variables take precedence.
|
||||
|
||||
```dotenv
|
||||
DB_USER=moonminers # app user: read/write access to the tables
|
||||
DB_PASSWORD=moonminers
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=moonminers
|
||||
ADMIN_DB_USER=postgres # superuser, only used by `migrate`
|
||||
ADMIN_DB_PASSWORD=postgres
|
||||
```
|
||||
|
||||
Optional: `PORT` (default 3000), `DB_SSLMODE` (default `disable`), `MM_DEV=1` (reload templates
|
||||
and static files from disk; also makes everyone an admin), `ADMIN_USER_IDS` (comma-separated
|
||||
player ids allowed on the admin pages; unset means no admins), `TRUST_PROXY=1` (behind a reverse proxy: rate-limit new
|
||||
players by the last `X-Forwarded-For` address instead of the connection's; new players are
|
||||
limited to 10 a minute per IP).
|
||||
|
||||
## Local development
|
||||
|
||||
1. Create `.env` as above.
|
||||
2. Start Postgres and set up the database (roles, schema, migrations, store items from
|
||||
`config/game.json`):
|
||||
1. Create `.env` (in the repo root or in `server/`; the variables are in `server/README.md`).
|
||||
2. Start the database and set it up:
|
||||
|
||||
```bash
|
||||
podman-compose up -d db # or: docker compose up -d db
|
||||
cd server
|
||||
make migrate
|
||||
```
|
||||
|
||||
3. Run the server and open http://localhost:3000:
|
||||
3. `make dev` (in `server/`) and open http://localhost:3000.
|
||||
|
||||
```bash
|
||||
make dev
|
||||
```
|
||||
`make build` and `make check` also run in `server/`.
|
||||
|
||||
Template, CSS and JS changes show up on reload; Go changes need a restart.
|
||||
## Run the Defold client
|
||||
|
||||
Other commands: `make build` (CSS + `bin/moonminers`), `make check` (gofmt, vet, the template
|
||||
check in `templates_test.go`, which renders every page against sample game states without a
|
||||
database, JS syntax, and `scripts/check-moon-traits.mjs`, which checks that `moondesc.go` and
|
||||
`terrain.js` agree on moon looks), `make load-config` (sync the store items in the database with `config/game.json`).
|
||||
Needs Java 21+; see `client/AGENTS.md` for the tools the scripts use.
|
||||
|
||||
The binary's commands: `serve` (default), `migrate`, `load-config`, `sim [file]` (print the
|
||||
economy analysis and a 4-hour greedy-player sim for the game config, or for a config file),
|
||||
and `moon-traits` (JSON on stdin; used by the moon traits check).
|
||||
Reset the local database: `podman-compose down -v && podman-compose up -d db && make migrate`.
|
||||
```bash
|
||||
cd client
|
||||
make run # against http://localhost:3000
|
||||
make run ARGS="--config=mm.api_base=http://localhost:3001"
|
||||
make web && make serve-web # browser build on :8080, proxying to :3000
|
||||
```
|
||||
|
||||
## Database changes
|
||||
## Deploy
|
||||
|
||||
`migrate` runs on every deploy and must be safe to repeat. It creates the `devs` role and the
|
||||
app user, creates the schema from `sql/tables.sql` and `sql/data.sql` on an empty database,
|
||||
applies `sql/migrations/*.sql` in name order, grants the app user access, and syncs the
|
||||
store items with the game config.
|
||||
|
||||
For a schema change, update `sql/tables.sql` (fresh databases) and add an idempotent
|
||||
`sql/migrations/NNN-name.sql` (existing databases: `if not exists`, `if exists`). A fresh
|
||||
database and one migrated from the pre-Go schema through every migration must end up with the
|
||||
same schema (compare `pg_dump --schema-only`). The migrations so far: 001 drops the old config
|
||||
tables, 002 moon settings, 003 config overrides, 004 live ops, 005 refinery, 006 player
|
||||
admin, 007 drill cycles in seconds, 008 security fixes (UTC defaults, one claim per stake,
|
||||
non-negative well supply), 009 schema cleanup (old columns, moon images), 010 drill
|
||||
attachments and advisors, 011 market pressure, 012 onboarding, 013 moon type
|
||||
(`staking_source.kind`, now that moons get generated names).
|
||||
|
||||
## Game config
|
||||
|
||||
`config/game.json` holds the balance numbers (starting credits, resources and tools, moon
|
||||
price, well supply range, `wellCountWeights`: how many wells a new moon gets, resource prices
|
||||
in credits per unit), the store items with their
|
||||
upgrade tiers (tiers can also need refined `parts`; a drill cycle is `completionTimeInSecs`),
|
||||
the moon types (a name, a placeholder description: the look is procedural and the
|
||||
description is generated when the moon is bought, see `moondesc.go`, and `affinities`:
|
||||
weights of the resources its wells hold, 1 if unlisted), moon names (`moonNames`: roots, and
|
||||
weights for the suffix styles: Roman numeral, Greek letter, catalogue number, letter and
|
||||
number; a player never gets the same name twice), and the refinery
|
||||
(`refinery`: slot prices, max batch size, `bulkExponent` (a batch of n takes
|
||||
seconds × n^bulkExponent, so bigger batches are faster per unit), and the refined items with
|
||||
their recipe, seconds per unit and sell price). Market prices move: every 5 minutes each resource and refined item
|
||||
drifts within ±`marketVolatility` (default 15%) of its configured price, from a deterministic
|
||||
function of name and time (`market.go`), which also gives the 24h charts; sells use it.
|
||||
A player's own selling lowers their price a little (`marketPressure`): each sale adds its
|
||||
base value to their pressure on the item, which halves every `halfLifeSecs`, and the price
|
||||
factor is `floor + (1 − floor) × e^(−pressure / scale)`, so it flattens out at `floor`.
|
||||
Drill attachments (`attachments`: `slotsPerTool`, and per attachment a `price`, an `image`
|
||||
and `speed`/`yield` multipliers) are bought in the Store and equipped on a drill from its
|
||||
panel in Equipment, where idle drills and spare attachments can also be sold back for
|
||||
`equipmentResale.factor` of their credit price (a drill: its price plus its upgrades; refined
|
||||
parts aren't refunded). A moon sells back (`moonResale`) for what's left in its wells at market
|
||||
price × `factor`, at most `maxRatio` × the price of a replacement moon, no sooner than
|
||||
`minHoldSecs` after buying it, and not while a drill on it is running. Advisors (`advisors`: `seatPrices` for the 2nd, 3rd, ... seat, and per
|
||||
advisor a hire `price` and `speed`/`yield`/`sell`/`refine` multipliers) boost the whole
|
||||
account. A player's multipliers multiply together and each kind is capped by `bonusCaps`;
|
||||
live events multiply on top. They count when an action runs, so a stake keeps the bonuses
|
||||
it started with (`bonus.go`).
|
||||
It's embedded in the binary, so changes need a rebuild.
|
||||
Store items are also kept in the database (`migrate` and `make load-config` sync them;
|
||||
items removed from the file are deleted unless a player owns one).
|
||||
|
||||
Admins can change these numbers without a deploy on `/utils/economy` (see Admin panels).
|
||||
|
||||
## Admin panels
|
||||
|
||||
Admins are the player ids listed in `ADMIN_USER_IDS` (comma-separated). With `MM_DEV=1`
|
||||
everyone is an admin, so never set it in production. Other players get a 403 (the admin pages
|
||||
show "missing permission"). Every admin page
|
||||
shares one nav: Players · Moons · Economy · Live ops · Game.
|
||||
|
||||
| Page | What it's for |
|
||||
|---|---|
|
||||
| `/utils` | Redirects to `/utils/players`. |
|
||||
| `/utils/players` | Find players (id prefix or name), sort by last seen, activity, newest, richest. |
|
||||
| `/utils/players/{id}` | One player: balance, resources, refined items, moons and wells, tools and drills, event history (older pages at `/utils/players/{id}/events?page=n`). Grant/remove credits, grant resources and tools, finish running drills, reset, ban/unban. |
|
||||
| `/utils/moons` | Moon lab: live 3D preview with sliders for terrain, craters, colors, bloom, atmosphere and rotation. "Save for the game" applies the look to every moon. |
|
||||
| `/utils/economy` | Edit the game config without a deploy: tables for equipment (with upgrade tiers), resources, refined items, market, attachments, advisors, moon types and constants, saved together as a draft; live analysis of the draft, version history with revert/reset, raw JSON. |
|
||||
| `/utils/liveops` | Dashboard, timed events and announcements. |
|
||||
|
||||
Actions (all `POST`, confirmed in the page's own dialog):
|
||||
|
||||
| Endpoint | Actions |
|
||||
|---|---|
|
||||
| `/utils/players/{id}/{action}` | `credits`, `resources`, `tool`, `finish-stakes`, `reset`, `ban`, `unban` |
|
||||
| `/utils/economy/{action}` | `analyze`, `save`, `revert`, `reset` |
|
||||
| `/utils/liveops/{what}` | `events/save`, `announcements/save` |
|
||||
| `/utils/liveops/{what}/{id}/{action}` | toggle or delete an event, delete an announcement |
|
||||
| `/utils/{action}` | `clear-data`, `load-config` (older endpoints, no page) |
|
||||
| `/api/moon-settings` | `GET` (public) returns the saved moon look; `POST` (admins) saves it |
|
||||
|
||||
Request examples are in `test-endpoints.restclient`.
|
||||
|
||||
### Players
|
||||
|
||||
Every admin action is logged in `event_log` with the admin's id (`admin_*` kinds, which the
|
||||
live-ops stats ignore). Banned players see "This account is suspended" and their actions get a
|
||||
403.
|
||||
|
||||
### Moons
|
||||
|
||||
Each moon's look is generated from its id (terrain technique, noise, craters, colors). The lab
|
||||
edits the shared settings that every moon starts from; they're stored as one JSON row in
|
||||
`moon_settings` and merged over the defaults in `web/static/js/terrain.js`.
|
||||
|
||||
### Economy
|
||||
|
||||
A form for the constants, resource prices, store items and upgrade tiers (including parts),
|
||||
and the refinery, plus a raw JSON editor for every field. The analysis updates as you type:
|
||||
income and payback per tool and upgrade, refining gain per recipe, red flags, and a 72-hour
|
||||
greedy-player sim. Each save stores the values that differ from `config/game.json` as a new row
|
||||
in `config_override`; the latest row is in effect (merged over the file, reloaded every 30 s)
|
||||
and store items are re-synced.
|
||||
|
||||
### Live ops
|
||||
|
||||
- Dashboard: players, daily actives, D1/D7 retention, actions, credits earned vs spent,
|
||||
resources claimed. Built from `event_log`, which every game action writes to.
|
||||
- Timed events: yield, sell price or mining speed multipliers, applied server-side.
|
||||
- Announcements.
|
||||
|
||||
Live events and announcements show in a banner at the top of the game. Times are UTC.
|
||||
|
||||
## Interface
|
||||
|
||||
Server-rendered regions swapped by htmx, which morphs them into the page with the idiomorph
|
||||
extension (0BSD, `web/static/vendor/idiomorph`) so elements keep their state; the JS only
|
||||
adds display: `main.js` (htmx glue,
|
||||
viewport, countdowns), `fx.js` (effects per action result), `sfx.js` (sound, below),
|
||||
`tabs.js` (sliding tab highlight), `enter.js` (entrance animations when a view changes),
|
||||
`market.js` (chart crosshair), `ui.js` (confirm dialog and toasts, shared with the admin
|
||||
pages), `scene.js`/`terrain.js`/`moon-worker.js` (the moon).
|
||||
|
||||
## Sound
|
||||
|
||||
`web/static/js/sfx.js` plays sound effects with the Web Audio API: short clips from Kenney's
|
||||
CC0 packs (trimmed and loudness-matched, listed in `web/static/assets/sfx/CREDITS.txt`) and
|
||||
a few sounds synthesized with ZzFX (MIT, `web/static/vendor/zzfx`). The speaker button in the
|
||||
header mutes them (remembered per browser); they are also muted while the tab is hidden.
|
||||
`window.mmAudio.mute()` / `unmute()` mute from outside (e.g. during an ad) without touching
|
||||
the player's setting. Set `localStorage["mm-sfx-debug"] = "1"` to log every sound played.
|
||||
|
||||
## Security
|
||||
|
||||
- Players are identified only by the `mm_user` cookie (HttpOnly, SameSite=Lax). New players
|
||||
are rate-limited to 10 a minute per IP (`TRUST_PROXY=1` behind a proxy).
|
||||
- Request bodies are capped at 64 KB (1 MB for `/utils/economy/*`); forms are parsed up front,
|
||||
so oversized ones get a 413.
|
||||
- Every response sets `X-Content-Type-Options: nosniff` and `Referrer-Policy: same-origin`.
|
||||
- Game actions validate ids, lock what they read and keep balances and supplies at 0 or more
|
||||
(schema checks back this up); a stake is claimed at most once (unique index).
|
||||
- Errors that aren't rule violations are logged and answered with a generic 500.
|
||||
|
||||
## Deployment (Dokploy)
|
||||
|
||||
`docker-compose.prod.yml` runs `db` (Postgres 16, data in the `db-data` volume), `migrate`
|
||||
(schema and migrations, runs before the app on every deploy) and `app` (port 3000).
|
||||
|
||||
First deploy:
|
||||
|
||||
1. Dokploy: create a **Docker Compose** service from this repository, compose path
|
||||
`docker-compose.prod.yml`.
|
||||
2. **Environment**:
|
||||
|
||||
```dotenv
|
||||
POSTGRES_PASSWORD=... # strong; Postgres superuser, used by migrate
|
||||
DB_PASSWORD=... # strong; app user "moonminers"
|
||||
ADMIN_USER_IDS= # fill in at step 5
|
||||
```
|
||||
|
||||
Never set `MM_DEV` in production: it makes every player an admin.
|
||||
3. **Domains**: your domain → service `app`, port `3000`, HTTPS on.
|
||||
4. Coming from the old (Next.js) version and want a clean start? Stop the service and delete
|
||||
the `db-data` volume first. Otherwise `migrate` upgrades the old database in place.
|
||||
5. Deploy, open the site once, and copy your player id: the value of the `mm_user` cookie
|
||||
(browser dev tools → Storage/Application → Cookies). Put it in `ADMIN_USER_IDS`
|
||||
(comma-separate several) and redeploy. The admin pages are at `/utils`.
|
||||
|
||||
Updates: push to the branch Dokploy tracks and deploy; `migrate` applies new migrations.
|
||||
Back up `db-data` with `pg_dump` or Dokploy's volume backups.
|
||||
|
||||
To test the production stack locally:
|
||||
`POSTGRES_PASSWORD=x DB_PASSWORD=y podman-compose -p mmprod -f docker-compose.prod.yml up --build`
|
||||
(add a `ports: ["3100:3000"]` override for `app` to reach it).
|
||||
|
||||
## Endpoints
|
||||
|
||||
See `test-endpoints.restclient` (Emacs restclient format).
|
||||
Dokploy: a Docker Compose service from this repository, compose path
|
||||
`docker-compose.prod.yml`. Environment, domains and first-deploy steps: `server/README.md`
|
||||
"Deployment".
|
||||
+5
-5
@@ -2,7 +2,7 @@
|
||||
|
||||
## What this is
|
||||
A native client for Moon Miners, the idle mining game whose rules and state live in the Go
|
||||
server (the repo root `..`, see `../AGENTS.md`). Same layout and HUD look as the web build
|
||||
server (`../server`, see `../server/AGENTS.md`; the whole repo: `../AGENTS.md`). Same layout and HUD look as the web build
|
||||
(header with sections, resources and credits; moon list on the left; the current section
|
||||
in the middle; aside with the moon viewport and the selected well), made game-native.
|
||||
Target: 16:10 at 1280x800 (Steam Deck); bigger windows show the UI at native pixel size
|
||||
@@ -11,7 +11,7 @@ with more room, smaller ones rearrange (see Scaling); mouse now, gamepad focus l
|
||||
The server is the authority. The client shows the state the server sends and sends
|
||||
actions. It may show an action's result before the answer (prediction, see Conventions),
|
||||
computed only from numbers the server sent (`take`, the tool's `cycleSecs`/`yield`, quotes,
|
||||
`rules`; `../api.md` "Prediction"); the next response replaces it. No other game
|
||||
`rules`; `../server/api.md` "Prediction"); the next response replaces it. No other game
|
||||
rules in Lua. Countdowns may tick locally as display only.
|
||||
|
||||
## Stack
|
||||
@@ -28,7 +28,7 @@ rules in Lua. Countdowns may tick locally as display only.
|
||||
the whole gui script, so closing a dialog (its own instance) left the game deaf to mouse
|
||||
and keys until the next rebuild. Don't post acquire/release_input_focus anywhere else.
|
||||
- Druid and Defold GUI findings (Layout, Data List, hover, limits, markup):
|
||||
`../druid-notes.md`. Druid source: `~/Development/Refold/druid`, Defold source:
|
||||
`../../druid-notes.md` (in the workspace, outside the repo). Druid source: `~/Development/Refold/druid`, Defold source:
|
||||
`~/Development/Refold/Defold` (read-only).
|
||||
|
||||
## Game screen: core, regions, sections
|
||||
@@ -517,7 +517,7 @@ the others on the command line.
|
||||
dialog) and images split font batches.
|
||||
- Outline textures use slice9 2 (not 1).
|
||||
- `api.lua`: `api.get_state(params, cb)`, `api.act(name, params, cb)`; `cb(err, data,
|
||||
status)`. The player id travels in `X-MM-User`. API: `../api.md`.
|
||||
status)`. The player id travels in `X-MM-User`. API: `../server/api.md`.
|
||||
- The server is the only source of truth: every response's `state` replaces ours and
|
||||
`state.view` becomes our view; actions send the current view plus their params and
|
||||
`clientSeq`. Action buttons go through `H.action(g, b, nodes, name, params, on_done,
|
||||
@@ -535,4 +535,4 @@ the others on the command line.
|
||||
- After a visual change, look at it: `make drive SCENARIO=sections MM_API=...` (or
|
||||
`views.sh` against a baseline) at 1280x800 and 2560x1600, and the dialog when you touch
|
||||
it. Run `make check` before calling it done.
|
||||
- Keep changes small; don't touch the server (`..`) from here.
|
||||
- Keep changes small; don't touch the server (`../server`) from here.
|
||||
@@ -2,7 +2,7 @@
|
||||
--
|
||||
-- The server still decides. A predictor only reuses numbers the server sent (stake
|
||||
-- `take`, the tool's `cycleSecs`/`yield`, the market quote and `rules.pressure`, ...,
|
||||
-- see ../api.md "Prediction"); it never invents game rules. The game screen keeps
|
||||
-- see ../server/api.md "Prediction"); it never invents game rules. The game screen keeps
|
||||
-- the last server state as the base and shows base + every pending prediction, re-applied
|
||||
-- in click order whenever a response brings a new base. A response drops its own
|
||||
-- prediction (200: the server's state has the real result; 422/network error: rolled back).
|
||||
|
||||
@@ -10,7 +10,7 @@ import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||
const server = process.env.MM_SERVER || path.join(root, "..");
|
||||
const server = process.env.MM_SERVER || path.join(root, "..", "server");
|
||||
const T = await import(pathToFileURL(path.join(server, "web/static/js/terrain.js")));
|
||||
const [res = "32", ...rest] = process.argv.slice(2);
|
||||
const seeds = rest.length ? rest : ["1042", "1187", "1301", "1555", "29dea5d5-709d-4c55-a269-cd90e4379fe9"];
|
||||
|
||||
@@ -9,7 +9,7 @@ x-db-env: &db-env
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
build: ./server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
<<: *db-env
|
||||
@@ -45,7 +45,7 @@ services:
|
||||
# One-shot, before the app starts on every deploy: roles, schema, migrations and the
|
||||
# store item sync from config/game.json (see migrate.go).
|
||||
migrate:
|
||||
build: .
|
||||
build: ./server
|
||||
command: ["migrate"]
|
||||
restart: "no"
|
||||
environment:
|
||||
|
||||
@@ -7,4 +7,3 @@ node_modules
|
||||
.DS_Store
|
||||
web/static/css/app.css
|
||||
art
|
||||
client
|
||||
@@ -0,0 +1,18 @@
|
||||
# Build output
|
||||
/bin/
|
||||
/moonminers
|
||||
/web/static/css/app.css
|
||||
|
||||
# Local env files (never commit)
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# Personal files
|
||||
/sql/create-users.sql
|
||||
sql/queries.sql
|
||||
/.dir-locals.el
|
||||
|
||||
# Generated by the art scripts
|
||||
/art/equipment/build/
|
||||
/art/advisors/build/
|
||||
/art/attachments/build/
|
||||
@@ -0,0 +1,188 @@
|
||||
# AGENTS.md — Moon Miners server
|
||||
|
||||
The Go server and the web client of Moon Miners. What the game is, the repo layout and the
|
||||
shared rules: `../AGENTS.md`. Run every command below from this directory (`server/`).
|
||||
|
||||
## Stack
|
||||
- Go (standard library `net/http` + `html/template`) with `pgx` for Postgres. One `main`
|
||||
package in `server/`; templates, static files and SQL are embedded with `go:embed`.
|
||||
- The interface is server-rendered HTML. htmx swaps it in place, morphing the game's regions
|
||||
(idiomorph extension, vendored in `web/static/vendor/idiomorph`) so elements keep their
|
||||
state; Tailwind v4 (standalone CLI) builds the CSS. No bundler, no TypeScript, no Alpine.
|
||||
- three.js is a plain ES module loaded through an import map (vendored in `web/static/vendor`).
|
||||
The viewport in the aside shows the selected moon, generated procedurally from its id
|
||||
(`terrain.js`, run in a worker by `moon-worker.js`; settings tuned on `/utils/moons` and
|
||||
stored in `moon_settings`). The HTML is the interface; the moon is only decoration.
|
||||
- Postgres 16, stock image. No functions or extensions in the database.
|
||||
|
||||
## Layout
|
||||
- This is `server/` in the repo (`../AGENTS.md`); the Defold client is `../client` (its own
|
||||
AGENTS.md), the compose files are at the repo root.
|
||||
- `main.go`: commands (`serve`, `migrate`, `load-config`, `sim [file]`, `moon-traits`), `.env`
|
||||
loading. `balance.go`: the `sim` command (prints the `sim.go` analysis).
|
||||
- `game.go`: game rules. Each action is one transaction that locks (`for update`) the rows it
|
||||
reads; `check (balance >= 0)` in the schema is the safety net. Rule violations return a
|
||||
`GameError` (shown to the player as a toast); other errors are 500s. Past moons (wells
|
||||
empty, nothing unclaimed) are computed (`loadPastMoons`, no column); they still count as
|
||||
owned for the moon price and well scale, and can't be sold. The Past tab loads them 10 at a
|
||||
time (`loadPastPage`, limit/offset; `page=` in the view URL); handlers load state with
|
||||
`loadView` (state, the past page, the resolved view).
|
||||
- `views.go`: the `View` (section, moon, well, tab; carried in the URL as
|
||||
`/?s=&moon=&well=`), the `Page` view model, formatting helpers.
|
||||
- `server.go`: routes and middleware (request log, `nosniff`/`Referrer-Policy` headers, body
|
||||
limits with forms parsed up front, a per-IP limit on new players). `GET /` renders the page
|
||||
(htmx requests get every region as an out-of-band swap); `POST /act/<action>` runs an
|
||||
action and responds the same way.
|
||||
- `api.go`, `api.md`: JSON API for non-web clients (Defold): `GET /api/state` (the view
|
||||
models mapped to JSON DTOs; times in unix ms) and `POST /api/act/<action>` (same
|
||||
`runAction` as `/act/`, gates included; `{ok, error, toast, fx, state}`, rule errors 422).
|
||||
Player id from `X-MM-User` or the cookie, else a new player (id echoed back).
|
||||
`api_sections.go`: every section's block (equipment and drill panel, store, refinery,
|
||||
advisors, leaderboard, help, new-moon teaser, reveal and buy modal in act responses, moon
|
||||
sale, market chart), plus `rules` (numbers for client prediction), `catalog` (static data,
|
||||
image URLs, tool codes), `jobs`, `bonuses`, `liveEvents`; blocks come with `s=` or
|
||||
`include=`; reasons are codes with the web's text beside them (`whyText`, …), confirm dialogs as `confirm`; `catalog=0` or `catalog=<catalogVersion>` skips the catalog; `clientSeq` is echoed. A new section in the API:
|
||||
map its view model in `addSection`. `api_test.go` (in `make check`) builds every block
|
||||
for every test view.
|
||||
- `config/game.json`: the game config (starting credits/resources/tools, moon price, well
|
||||
supply, `wellCountWeights`, resource prices, store items with upgrade tiers, moon types: a
|
||||
name, a placeholder description and resource `affinities`, refinery), embedded.
|
||||
`rollWells` (game.go) rolls a new moon's wells: 2–5 by weight, resources by its type's
|
||||
affinities, each well's supply rolled in [`wellSupplyMin`, `wellSupplyMax`) (fewer wells
|
||||
hold less); a first moon
|
||||
always has every raw input of a recipe. A moon's name comes from `moonNames` (roots and
|
||||
suffix style weights, `rollMoonName`), never repeated among a player's moons; its type is
|
||||
`staking_source.kind` (moons from before migration 013 were named after their type).
|
||||
`config.go` parses it into `game` and syncs store items into `store_item`/`upgrade_item`
|
||||
(needed for the inventory foreign key and the SQL in game.go). Admins override its
|
||||
values live on `/utils/economy` (table editor in `economy.js`; `economy.go`: a patch per
|
||||
save in `config_override`, latest wins; `sim.go`: balance analysis and progression sim). Read config values through
|
||||
`cfg()` (game.json + override), never `game` directly. `migrate.go`: database setup.
|
||||
- `db.go`: connections, `inTx`, `GameError`.
|
||||
- `market.go`: market prices. A unit sells for its config price × a deterministic drift
|
||||
(name + 5-minute tick, within ±`marketVolatility`) × live price events × the player's
|
||||
selling pressure (`marketPressure`: their recent sales of the item, in base-price credits,
|
||||
decaying with a half-life, table `market_pressure`; the factor eases from 1 towards
|
||||
`floor`, and a sale is priced at its average over the pressure it adds). `marketRate` is
|
||||
used by `sell`/`sellRefined` and the market view (24h history recomputed, no table);
|
||||
view model for the Market section (ticker, watchlist, chart, sell ticket; `pick=` in the
|
||||
URL selects a quote). `web/static/js/market.js`: chart crosshair and ticket estimate.
|
||||
- `resale.go`: selling a moon back (`moonResale` in game.json): what's left in its wells at
|
||||
market price (with live events, without selling pressure or advisors) × `factor`, capped
|
||||
at `maxRatio` × the replacement moon's price (`moonPriceFor(owned-1)`), which it's
|
||||
compared with. Not the last moon, not within `minHoldSecs` of buying it, not while a
|
||||
drill on it is running; finished drills are claimed in the sale. Template: `moon-sale`.
|
||||
Also selling tools and spare attachments for `equipmentResale.factor` (below 1) of their
|
||||
credit price (a tool: price + its upgrades; refined parts not refunded): only idle
|
||||
tools with nothing to claim, never the last tool; attachments on it return to the inventory.
|
||||
- `refinery.go`: refinery config (`refinery` in game.json, `parts` on upgrade tiers), rules
|
||||
(start batch, collect, buy slot, sell refined, upgrade parts) and its view model. A batch
|
||||
of n takes `seconds` × n^`bulkExponent` (`batchSecs`). Tables
|
||||
`refined_account`, `refinery_job`, `users.refinery_slots`. Template: `refinery.html`.
|
||||
- `bonus.go`: player bonuses. `bonusesFor` combines advisors and a tool's attachments
|
||||
(multipliers multiply, then each kind is capped by `bonusCaps`); live events multiply on
|
||||
top, uncapped. `drillStats` turns a tool's cycle and yield into a stake's (used by
|
||||
`stakeInTx`, the views and the sim); sells and refinery batches use `Bonus.Sell`/`Refine`.
|
||||
Bonuses are fixed when the action runs (a stake keeps its duration and amount).
|
||||
- `attachments.go`: attachments config (`attachments` in game.json: `slotsPerTool`, items
|
||||
with `speed`/`yield` multipliers), buy/equip/unequip rules (one of each per tool; equip
|
||||
and unequip any time, they count from the next stake), store cards and the drill panel
|
||||
(`drill=` in the URL, Equipment section, in the aside). Table `attachment`
|
||||
(`inventory_item_id` null: unequipped).
|
||||
- `advisors.go`, `web/templates/advisors.html`: advisors config (`advisors`), hire, buy
|
||||
seat, seat/unseat/swap/dismiss rules (only seated advisors give a bonus; dismissing
|
||||
refunds nothing), Advisors section and the advisor panel (`adv=` in the URL, in the
|
||||
aside). Tables `advisor_hire` (`seat`, null: not seated), `users.advisor_seats`.
|
||||
- `onboarding.go`: onboarding by progressive reveal, no tutorial text. `users.onboarding`
|
||||
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
|
||||
reveals Equipment, claiming Market, selling Store (and "+ New moon", Help, upgrades,
|
||||
attachments, moon sale, Redrill), claiming both raw inputs of a recipe Refinery, buying a
|
||||
tool or attachment Advisors. Templates ask `.S.Unlocked "<feature>"`; `View.resolve` sends
|
||||
locked sections to the moon view and `runAction` refuses locked actions (`actionGates`).
|
||||
A revealed tab shines (`data-shine`) until opened (`markSeen` in `handleIndex`). New
|
||||
sections and actions need a gate there.
|
||||
- `players.go`, `web/templates/players.html`: admin Players page at `/utils/players`
|
||||
(search, detail, grants, finish drills, reset, onboarding reset/unlock all, ban; each logged as `admin_*` in
|
||||
`event_log`). `notBanned` gives banned players (`users.banned_at`) a suspended page.
|
||||
`web/templates/admin.html`: the shared admin nav (`{{template "admin-nav" "<page>"}}`, put
|
||||
it on every `/utils` page) and confirm dialog. `/utils` redirects to `/utils/players`.
|
||||
- `web/templates/`: `page.html` (page shell, boot page), `app.html` (header, moon
|
||||
list, sections, side panel).
|
||||
- `web/static/js/main.js`: glue between htmx and the scene (and what a morph keeps of the JS's
|
||||
own changes: classes, styles, inserted elements, typed values), and countdowns (`data-ends`)
|
||||
that enable their Claim button (`data-ready-at`) when due. `scene.js`: the three.js scene.
|
||||
`tabs.js`: sliding tab highlight (`data-tabs`). `enter.js`: entrance animations on view
|
||||
changes. `ui.js`: the custom confirm dialog (`hx-confirm`) and toasts, shared by the game
|
||||
and admin pages.
|
||||
- `web/static/js/fx.js`: effects per action result (toast `data-fx`). `sfx.js`: sound
|
||||
(Web Audio; clips in `web/static/assets/sfx`, see CREDITS.txt; synth sounds via vendored
|
||||
ZzFX). fx.js maps action kinds to sounds; mute is in the header, `window.mmAudio` for
|
||||
outside control (e.g. ads).
|
||||
`terrain.js`: moon generation (no imports, also runs in the worker). `moons-admin.js` and
|
||||
`web/templates/moons.html`: the moon lab. `moons.go`: moon settings endpoints, `isAdmin`.
|
||||
`moondesc.go`: moon descriptions generated from the look (mirrors terrain.js `moonTraits`;
|
||||
`scripts/check-moon-traits.mjs` in `make check` keeps them in sync) and the wells.
|
||||
- `reveal.go`, `web/static/js/reveal.js`: the new-moon section (`s=new-moon`, a teaser with a
|
||||
shrouded moon in the viewport, `mystery` in the scene data) and the reveal after
|
||||
`buy-moon` (the response carries `#reveal` with the moon's card; scene.js plays the
|
||||
burst, fx.js waits for `mm:revealed` before counting the balance down).
|
||||
`rating.go`: a new moon's rating, the percentile of its base-price value among a cached
|
||||
sample of `rollWells` (resampled when the config changes), stored in
|
||||
`staking_source.rating`; tiers Poor/Fair/Good/Great/Exceptional drive the card's verdict
|
||||
and the star chip in the moon list and header.
|
||||
- `liveops.go`, `events.go`, `web/templates/liveops.html`, `web/static/js/liveops.js`: live
|
||||
ops at `/utils/liveops` (dashboard from `event_log`, timed events in `live_event` applied
|
||||
via `eventMultiplier` in game.go, `announcement`; both shown in the game's `#banner`).
|
||||
- `web/css/app.css`: Tailwind input (custom HUD styles); builds to `web/static/css/app.css`.
|
||||
- `sql/tables.sql`, `sql/data.sql`: schema for fresh databases. `sql/migrations/` (001–016):
|
||||
changes for existing ones, applied on every `migrate`, so each must be idempotent.
|
||||
- `test-endpoints.restclient`: the up-to-date list of endpoints, in Emacs restclient format.
|
||||
- `art/`: sources of the store, attachment and advisor images (Blender scripts, prompts);
|
||||
they write into `web/static/assets`.
|
||||
|
||||
## Commands
|
||||
- `make dev`: server on :3000 that reloads templates/static from disk, with Tailwind watching.
|
||||
- `make build`, `make check` (gofmt, vet, template check, JS syntax of every `web/static/js/*.js`,
|
||||
moon traits check), `make migrate`, `make load-config`. `go run . sim` prints the economy analysis.
|
||||
- Template check: `templates_test.go` (`go test -run Templates`, in `make check`) parses the
|
||||
templates like the server and renders every page and region against in-memory game states;
|
||||
when you add a page, section or view model branch, make sure its states/views reach it.
|
||||
- Local DB: `podman-compose up -d db` (from the repo root) then `make migrate` (see README).
|
||||
There is no other test suite, except `wells_test.go` (`go test -run Wells -v`: well roll
|
||||
distribution, value by well count, resale odds, sim seeds).
|
||||
|
||||
## Deployment
|
||||
- Production uses `../docker-compose.prod.yml` (at the repo root, build context `server/`;
|
||||
deployed with Dokploy): `app`, `db` (stock
|
||||
Postgres) and `migrate` (same image as `app`, runs on every deploy before the app starts).
|
||||
- Schema changes need both an update to `sql/tables.sql` (fresh DBs) and a new idempotent
|
||||
`sql/migrations/NNN-name.sql` (existing DBs). Check that a fresh DB and one built from the
|
||||
pre-Go schema (`git show 610d26e:sql/tables.sql`) plus all migrations give the same
|
||||
`pg_dump --schema-only`.
|
||||
- Keep `../docker-compose.prod.yml` services on the explicit `internal` network (Dokploy adds its own network).
|
||||
|
||||
## Environment
|
||||
Postgres credentials come from `.env` (in `server/`, else the repo root's `../.env`; only
|
||||
the first one found is read): `DB_USER`, `DB_PASSWORD`, `DB_HOST`, `DB_PORT`,
|
||||
`DB_DATABASE`, plus `ADMIN_DB_USER`/`ADMIN_DB_PASSWORD` for `migrate`. Never read, print or
|
||||
commit `.env`. `ADMIN_USER_IDS` (comma-separated player ids) grants access to `/utils`
|
||||
(unset: nobody); with `MM_DEV=1` everyone is an admin.
|
||||
|
||||
## Conventions
|
||||
- Game rules go in `game.go`, in a transaction, locking what they read. Keep handlers thin.
|
||||
- Layout: header, moon list, main area (one section at a time), and an aside with the 3D
|
||||
viewport and the selected well. Links are `<a href hx-get>` to a view URL; main.js keeps
|
||||
the address bar in sync.
|
||||
- New section: add it to `sections` and `State.page` (views.go) and a `<name>-section`
|
||||
template in `app.html`. New action: a case in `server.runAction` (shared by `/act/` and
|
||||
`/api/act/`) plus a button with `hx-post="/act/<action>"`. The current view (s, moon, well, drill) is sent automatically,
|
||||
so action parameters must use other names. Give it a sound in fx.js if it needs one.
|
||||
- Clicks give instant feedback only (busy state + spinner). No client-side game logic or
|
||||
prediction; the server is the authority. Pages don't reload on timers: countdown labels
|
||||
(`data-ends`) tick client-side and may enable a Claim button, as display only.
|
||||
- SQL is lowercase with snake_case identifiers.
|
||||
- When you add an endpoint, add it to `test-endpoints.restclient`.
|
||||
- New player action: log it with `logEvent(ctx, tx, userID, "<kind>", data)` inside its
|
||||
transaction (spends use the key `price`, earnings `credits`; the dashboard sums them).
|
||||
- Dispose any three.js geometry and materials you create.
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,256 @@
|
||||
# Moon Miners server
|
||||
|
||||
A browser mining game. Players own moons (staking sources) with resource wells, buy and
|
||||
upgrade mining tools, stake them on wells, claim resources when the stake completes, and sell
|
||||
resources for credits (₥). Drill cycles last seconds, so play is active. The refinery turns
|
||||
resources into refined items on a timer; those sell for more and are needed as parts for
|
||||
higher tool upgrades.
|
||||
|
||||
It's a Go server that renders the interface as HTML (with htmx for in-place updates) next to
|
||||
a three.js view of the selected moon, generated procedurally from its id. Game rules run in
|
||||
Go, one Postgres transaction per action. The page doesn't reload on timers: countdowns tick
|
||||
in the browser and enable each Claim button when its drill should be done, and the server
|
||||
decides what is actually ready when it's clicked.
|
||||
|
||||
Players join anonymously: on the first visit the server creates a player and stores its id in
|
||||
a cookie. No wallet or sign-up needed.
|
||||
|
||||
This is `server/` in the repo (the Defold client is `../client`). Run the commands below
|
||||
from this directory.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Go 1.25+
|
||||
- PostgreSQL 16 (stock image is fine), or Podman/Docker to run it
|
||||
- Node.js, only for `make check` (syntax-checks the JS)
|
||||
|
||||
`make` downloads the Tailwind standalone CLI into `bin/` on first use.
|
||||
|
||||
## Environment
|
||||
|
||||
The server reads `.env` from its working directory (`server/`), or if there is none, from the
|
||||
repo root (`../.env`); only one of them is read. It's gitignored, never commit it. Real
|
||||
environment variables take precedence.
|
||||
|
||||
```dotenv
|
||||
DB_USER=moonminers # app user: read/write access to the tables
|
||||
DB_PASSWORD=moonminers
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=moonminers
|
||||
ADMIN_DB_USER=postgres # superuser, only used by `migrate`
|
||||
ADMIN_DB_PASSWORD=postgres
|
||||
```
|
||||
|
||||
Optional: `PORT` (default 3000), `DB_SSLMODE` (default `disable`), `MM_DEV=1` (reload templates
|
||||
and static files from disk; also makes everyone an admin), `ADMIN_USER_IDS` (comma-separated
|
||||
player ids allowed on the admin pages; unset means no admins), `TRUST_PROXY=1` (behind a reverse proxy: rate-limit new
|
||||
players by the last `X-Forwarded-For` address instead of the connection's; new players are
|
||||
limited to 10 a minute per IP).
|
||||
|
||||
## Local development
|
||||
|
||||
1. Create `.env` as above.
|
||||
2. Start Postgres and set up the database (roles, schema, migrations, store items from
|
||||
`config/game.json`):
|
||||
|
||||
```bash
|
||||
(cd .. && podman-compose up -d db) # or: docker compose up -d db, at the repo root
|
||||
make migrate
|
||||
```
|
||||
|
||||
3. Run the server and open http://localhost:3000:
|
||||
|
||||
```bash
|
||||
make dev
|
||||
```
|
||||
|
||||
Template, CSS and JS changes show up on reload; Go changes need a restart.
|
||||
|
||||
Other commands: `make build` (CSS + `bin/moonminers`), `make check` (gofmt, vet, the template
|
||||
check in `templates_test.go`, which renders every page against sample game states without a
|
||||
database, JS syntax, and `scripts/check-moon-traits.mjs`, which checks that `moondesc.go` and
|
||||
`terrain.js` agree on moon looks), `make load-config` (sync the store items in the database with `config/game.json`).
|
||||
|
||||
The binary's commands: `serve` (default), `migrate`, `load-config`, `sim [file]` (print the
|
||||
economy analysis and a 4-hour greedy-player sim for the game config, or for a config file),
|
||||
and `moon-traits` (JSON on stdin; used by the moon traits check).
|
||||
Reset the local database (compose commands at the repo root): `podman-compose down -v && podman-compose up -d db`, then `make migrate` here.
|
||||
|
||||
## Database changes
|
||||
|
||||
`migrate` runs on every deploy and must be safe to repeat. It creates the `devs` role and the
|
||||
app user, creates the schema from `sql/tables.sql` and `sql/data.sql` on an empty database,
|
||||
applies `sql/migrations/*.sql` in name order, grants the app user access, and syncs the
|
||||
store items with the game config.
|
||||
|
||||
For a schema change, update `sql/tables.sql` (fresh databases) and add an idempotent
|
||||
`sql/migrations/NNN-name.sql` (existing databases: `if not exists`, `if exists`). A fresh
|
||||
database and one migrated from the pre-Go schema through every migration must end up with the
|
||||
same schema (compare `pg_dump --schema-only`). The migrations so far: 001 drops the old config
|
||||
tables, 002 moon settings, 003 config overrides, 004 live ops, 005 refinery, 006 player
|
||||
admin, 007 drill cycles in seconds, 008 security fixes (UTC defaults, one claim per stake,
|
||||
non-negative well supply), 009 schema cleanup (old columns, moon images), 010 drill
|
||||
attachments and advisors, 011 market pressure, 012 onboarding, 013 moon type
|
||||
(`staking_source.kind`, now that moons get generated names).
|
||||
|
||||
## Game config
|
||||
|
||||
`config/game.json` holds the balance numbers (starting credits, resources and tools, moon
|
||||
price, well supply range, `wellCountWeights`: how many wells a new moon gets, resource prices
|
||||
in credits per unit), the store items with their
|
||||
upgrade tiers (tiers can also need refined `parts`; a drill cycle is `completionTimeInSecs`),
|
||||
the moon types (a name, a placeholder description: the look is procedural and the
|
||||
description is generated when the moon is bought, see `moondesc.go`, and `affinities`:
|
||||
weights of the resources its wells hold, 1 if unlisted), moon names (`moonNames`: roots, and
|
||||
weights for the suffix styles: Roman numeral, Greek letter, catalogue number, letter and
|
||||
number; a player never gets the same name twice), and the refinery
|
||||
(`refinery`: slot prices, max batch size, `bulkExponent` (a batch of n takes
|
||||
seconds × n^bulkExponent, so bigger batches are faster per unit), and the refined items with
|
||||
their recipe, seconds per unit and sell price). Market prices move: every 5 minutes each resource and refined item
|
||||
drifts within ±`marketVolatility` (default 15%) of its configured price, from a deterministic
|
||||
function of name and time (`market.go`), which also gives the 24h charts; sells use it.
|
||||
A player's own selling lowers their price a little (`marketPressure`): each sale adds its
|
||||
base value to their pressure on the item, which halves every `halfLifeSecs`, and the price
|
||||
factor is `floor + (1 − floor) × e^(−pressure / scale)`, so it flattens out at `floor`.
|
||||
Drill attachments (`attachments`: `slotsPerTool`, and per attachment a `price`, an `image`
|
||||
and `speed`/`yield` multipliers) are bought in the Store and equipped on a drill from its
|
||||
panel in Equipment, where idle drills and spare attachments can also be sold back for
|
||||
`equipmentResale.factor` of their credit price (a drill: its price plus its upgrades; refined
|
||||
parts aren't refunded). A moon sells back (`moonResale`) for what's left in its wells at market
|
||||
price × `factor`, at most `maxRatio` × the price of a replacement moon, no sooner than
|
||||
`minHoldSecs` after buying it, and not while a drill on it is running. Advisors (`advisors`: `seatPrices` for the 2nd, 3rd, ... seat, and per
|
||||
advisor a hire `price` and `speed`/`yield`/`sell`/`refine` multipliers) boost the whole
|
||||
account. A player's multipliers multiply together and each kind is capped by `bonusCaps`;
|
||||
live events multiply on top. They count when an action runs, so a stake keeps the bonuses
|
||||
it started with (`bonus.go`).
|
||||
It's embedded in the binary, so changes need a rebuild.
|
||||
Store items are also kept in the database (`migrate` and `make load-config` sync them;
|
||||
items removed from the file are deleted unless a player owns one).
|
||||
|
||||
Admins can change these numbers without a deploy on `/utils/economy` (see Admin panels).
|
||||
|
||||
## Admin panels
|
||||
|
||||
Admins are the player ids listed in `ADMIN_USER_IDS` (comma-separated). With `MM_DEV=1`
|
||||
everyone is an admin, so never set it in production. Other players get a 403 (the admin pages
|
||||
show "missing permission"). Every admin page
|
||||
shares one nav: Players · Moons · Economy · Live ops · Game.
|
||||
|
||||
| Page | What it's for |
|
||||
|---|---|
|
||||
| `/utils` | Redirects to `/utils/players`. |
|
||||
| `/utils/players` | Find players (id prefix or name), sort by last seen, activity, newest, richest. |
|
||||
| `/utils/players/{id}` | One player: balance, resources, refined items, moons and wells, tools and drills, event history (older pages at `/utils/players/{id}/events?page=n`). Grant/remove credits, grant resources and tools, finish running drills, reset, ban/unban. |
|
||||
| `/utils/moons` | Moon lab: live 3D preview with sliders for terrain, craters, colors, bloom, atmosphere and rotation. "Save for the game" applies the look to every moon. |
|
||||
| `/utils/economy` | Edit the game config without a deploy: tables for equipment (with upgrade tiers), resources, refined items, market, attachments, advisors, moon types and constants, saved together as a draft; live analysis of the draft, version history with revert/reset, raw JSON. |
|
||||
| `/utils/liveops` | Dashboard, timed events and announcements. |
|
||||
|
||||
Actions (all `POST`, confirmed in the page's own dialog):
|
||||
|
||||
| Endpoint | Actions |
|
||||
|---|---|
|
||||
| `/utils/players/{id}/{action}` | `credits`, `resources`, `tool`, `finish-stakes`, `reset`, `ban`, `unban` |
|
||||
| `/utils/economy/{action}` | `analyze`, `save`, `revert`, `reset` |
|
||||
| `/utils/liveops/{what}` | `events/save`, `announcements/save` |
|
||||
| `/utils/liveops/{what}/{id}/{action}` | toggle or delete an event, delete an announcement |
|
||||
| `/utils/{action}` | `clear-data`, `load-config` (older endpoints, no page) |
|
||||
| `/api/moon-settings` | `GET` (public) returns the saved moon look; `POST` (admins) saves it |
|
||||
|
||||
Request examples are in `test-endpoints.restclient`.
|
||||
|
||||
### Players
|
||||
|
||||
Every admin action is logged in `event_log` with the admin's id (`admin_*` kinds, which the
|
||||
live-ops stats ignore). Banned players see "This account is suspended" and their actions get a
|
||||
403.
|
||||
|
||||
### Moons
|
||||
|
||||
Each moon's look is generated from its id (terrain technique, noise, craters, colors). The lab
|
||||
edits the shared settings that every moon starts from; they're stored as one JSON row in
|
||||
`moon_settings` and merged over the defaults in `web/static/js/terrain.js`.
|
||||
|
||||
### Economy
|
||||
|
||||
A form for the constants, resource prices, store items and upgrade tiers (including parts),
|
||||
and the refinery, plus a raw JSON editor for every field. The analysis updates as you type:
|
||||
income and payback per tool and upgrade, refining gain per recipe, red flags, and a 72-hour
|
||||
greedy-player sim. Each save stores the values that differ from `config/game.json` as a new row
|
||||
in `config_override`; the latest row is in effect (merged over the file, reloaded every 30 s)
|
||||
and store items are re-synced.
|
||||
|
||||
### Live ops
|
||||
|
||||
- Dashboard: players, daily actives, D1/D7 retention, actions, credits earned vs spent,
|
||||
resources claimed. Built from `event_log`, which every game action writes to.
|
||||
- Timed events: yield, sell price or mining speed multipliers, applied server-side.
|
||||
- Announcements.
|
||||
|
||||
Live events and announcements show in a banner at the top of the game. Times are UTC.
|
||||
|
||||
## Interface
|
||||
|
||||
Server-rendered regions swapped by htmx, which morphs them into the page with the idiomorph
|
||||
extension (0BSD, `web/static/vendor/idiomorph`) so elements keep their state; the JS only
|
||||
adds display: `main.js` (htmx glue,
|
||||
viewport, countdowns), `fx.js` (effects per action result), `sfx.js` (sound, below),
|
||||
`tabs.js` (sliding tab highlight), `enter.js` (entrance animations when a view changes),
|
||||
`market.js` (chart crosshair), `ui.js` (confirm dialog and toasts, shared with the admin
|
||||
pages), `scene.js`/`terrain.js`/`moon-worker.js` (the moon).
|
||||
|
||||
## Sound
|
||||
|
||||
`web/static/js/sfx.js` plays sound effects with the Web Audio API: short clips from Kenney's
|
||||
CC0 packs (trimmed and loudness-matched, listed in `web/static/assets/sfx/CREDITS.txt`) and
|
||||
a few sounds synthesized with ZzFX (MIT, `web/static/vendor/zzfx`). The speaker button in the
|
||||
header mutes them (remembered per browser); they are also muted while the tab is hidden.
|
||||
`window.mmAudio.mute()` / `unmute()` mute from outside (e.g. during an ad) without touching
|
||||
the player's setting. Set `localStorage["mm-sfx-debug"] = "1"` to log every sound played.
|
||||
|
||||
## Security
|
||||
|
||||
- Players are identified only by the `mm_user` cookie (HttpOnly, SameSite=Lax). New players
|
||||
are rate-limited to 10 a minute per IP (`TRUST_PROXY=1` behind a proxy).
|
||||
- Request bodies are capped at 64 KB (1 MB for `/utils/economy/*`); forms are parsed up front,
|
||||
so oversized ones get a 413.
|
||||
- Every response sets `X-Content-Type-Options: nosniff` and `Referrer-Policy: same-origin`.
|
||||
- Game actions validate ids, lock what they read and keep balances and supplies at 0 or more
|
||||
(schema checks back this up); a stake is claimed at most once (unique index).
|
||||
- Errors that aren't rule violations are logged and answered with a generic 500.
|
||||
|
||||
## Deployment (Dokploy)
|
||||
|
||||
`docker-compose.prod.yml` (at the repo root; it builds the image from `server/`) runs `db` (Postgres 16, data in the `db-data` volume), `migrate`
|
||||
(schema and migrations, runs before the app on every deploy) and `app` (port 3000).
|
||||
|
||||
First deploy:
|
||||
|
||||
1. Dokploy: create a **Docker Compose** service from this repository, compose path
|
||||
`docker-compose.prod.yml`.
|
||||
2. **Environment**:
|
||||
|
||||
```dotenv
|
||||
POSTGRES_PASSWORD=... # strong; Postgres superuser, used by migrate
|
||||
DB_PASSWORD=... # strong; app user "moonminers"
|
||||
ADMIN_USER_IDS= # fill in at step 5
|
||||
```
|
||||
|
||||
Never set `MM_DEV` in production: it makes every player an admin.
|
||||
3. **Domains**: your domain → service `app`, port `3000`, HTTPS on.
|
||||
4. Coming from the old (Next.js) version and want a clean start? Stop the service and delete
|
||||
the `db-data` volume first. Otherwise `migrate` upgrades the old database in place.
|
||||
5. Deploy, open the site once, and copy your player id: the value of the `mm_user` cookie
|
||||
(browser dev tools → Storage/Application → Cookies). Put it in `ADMIN_USER_IDS`
|
||||
(comma-separate several) and redeploy. The admin pages are at `/utils`.
|
||||
|
||||
Updates: push to the branch Dokploy tracks and deploy; `migrate` applies new migrations.
|
||||
Back up `db-data` with `pg_dump` or Dokploy's volume backups.
|
||||
|
||||
To test the production stack locally, at the repo root:
|
||||
`POSTGRES_PASSWORD=x DB_PASSWORD=y podman-compose -p mmprod -f docker-compose.prod.yml up --build`
|
||||
(add a `ports: ["3100:3000"]` override for `app` to reach it).
|
||||
|
||||
## Endpoints
|
||||
|
||||
See `test-endpoints.restclient` (Emacs restclient format).
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -23,7 +23,10 @@ import (
|
||||
var embedded embed.FS
|
||||
|
||||
func main() {
|
||||
loadDotEnv(".env")
|
||||
// .env in the working directory (server/), else the repo root's.
|
||||
if !loadDotEnv(".env") {
|
||||
loadDotEnv("../.env")
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
@@ -65,10 +68,11 @@ func runLoadConfig(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// loadDotEnv sets variables from a KEY=VALUE file, without overriding the real environment.
|
||||
func loadDotEnv(path string) {
|
||||
// It reports whether the file exists.
|
||||
func loadDotEnv(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
scanner := bufio.NewScanner(f)
|
||||
@@ -83,6 +87,7 @@ func loadDotEnv(path string) {
|
||||
os.Setenv(key, strings.Trim(strings.TrimSpace(value), `"'`))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func env(key, fallback string) string {
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 160 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user