@nano133/nanoterra 0.9.3 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +7 -5
- package/ai/SPEC.md +1 -1
- package/dist/admin/amount.d.ts +4 -0
- package/dist/admin/amount.js +4 -0
- package/dist/admin/panel/dom.d.ts +1 -1
- package/dist/admin/panel/dom.js +3 -2
- package/dist/admin/panel/shelf.js +1 -1
- package/dist/admin/panel/shelfData.d.ts +1 -1
- package/dist/admin/panel/shelfData.js +6 -6
- package/dist/admin/summary.d.ts +1 -1
- package/dist/admin/summary.js +8 -8
- package/dist/looks/index.d.ts +1 -1
- package/dist/looks/index.js +1 -1
- package/dist/looks/ladder.d.ts +1 -1
- package/dist/looks/ladder.js +7 -7
- package/dist/looks/look.d.ts +7 -2
- package/dist/looks/look.js +10 -3
- package/dist/shop/handler.js +2 -2
- package/package.json +1 -1
- package/src/admin/amount.ts +6 -0
- package/src/admin/panel/dom.ts +4 -2
- package/src/admin/panel/shelf.ts +1 -1
- package/src/admin/panel/shelfData.ts +6 -6
- package/src/admin/summary.ts +8 -8
- package/src/looks/index.ts +1 -1
- package/src/looks/ladder.ts +7 -6
- package/src/looks/look.ts +12 -3
- package/src/shop/handler.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 (2026-11-01)
|
|
4
|
+
|
|
5
|
+
- **No minimum price: a fraction of a cent works.** A look's `priceUsd` may have up to 4 decimals (`0.005` is half a cent, `0.0001` the smallest price
|
|
6
|
+
above 0); before, the looks check refused anything under a whole cent. New in `/looks`: `priceStepOk(usd)` (the new rule), `PRICE_STEP_USD`
|
|
7
|
+
(`0.0001`) and `usdDecimals(usd)`; `wholeCents` stays as it was. The shop already rounded the Nano amount up to a step of 0.0001 XNO (at least one
|
|
8
|
+
step), and the matcher compares exact amounts, so a half-cent order asks for 0.005 XNO at $1 and only that amount pays it.
|
|
9
|
+
- `usdText` writes 2 to 4 decimals (`"$0.25"`, `"$1.00"`, `"$0.005"`), so the Buy key and the pay sheet no longer show a sub-cent price as a rounded
|
|
10
|
+
cent; `priceText` writes `0.5¢`. An order keeps its dollars as `usdDecimals` (`"0.25"` and `"1.00"` as before, `"0.005"`), not `toFixed(2)`.
|
|
11
|
+
- The admin counts sales in steps of $0.0001, not cents (the Shelf, Earned, the best sellers), its price box takes 4 decimals, and the message for an
|
|
12
|
+
AI assistant says "at most 4 decimals".
|
|
13
|
+
|
|
3
14
|
## 0.9.3 (2026-11-01)
|
|
4
15
|
|
|
5
16
|
- **A session that ended at Buy.** A 401 from the server at Buy (the session ended, though the game's `signedIn()` still said yes) showed a
|
package/README.md
CHANGED
|
@@ -404,7 +404,7 @@ const text = looksJson(newLooks); // the new looks.json: one look on each line
|
|
|
404
404
|
type Look = {
|
|
405
405
|
id: string; // 1 to 40 lowercase letters, digits or dashes: "mint-cap"
|
|
406
406
|
name: string; // 1 to 40 characters
|
|
407
|
-
priceUsd: number; // US dollars, at most
|
|
407
|
+
priceUsd: number; // US dollars, at most 4 decimals (0.005 is half a cent; since 0.10.0); 0 is the free look
|
|
408
408
|
wear?: string; // the wear slot it fills: none (an outfit), or your game's own slot ("shovel"): see "Wear slots" below
|
|
409
409
|
change?: { body: string; hat?: "cap" | "beanie" | "crown" | "tophat"; hatColor?: string }; // an outfit's colours ("#rrggbb"); an item of another slot has none
|
|
410
410
|
slots?: Record<string, string>; // what YOUR game defines for itself: see "Slots" below. Optional.
|
|
@@ -478,8 +478,8 @@ The rules (the same as the starter's `AGENTS.md`):
|
|
|
478
478
|
| `diffLooks(before, after)` | `{ added, removed, changed }` by id. A new order is not a change. |
|
|
479
479
|
| `looksJson(looks)` | The text of `looks.json`, one look on each line, as the starter writes it. A change of one look is a change of one line. |
|
|
480
480
|
| `slugId(name, taken)` | A free id for a name: `"Mint Cap"` gives `mint-cap`, then `mint-cap-2`. |
|
|
481
|
-
| `PRICE_LADDER`, `rungOf(usd)`, `priceText(usd)` | The six steps nano133.com uses, from 1¢ to $1, and a price as a page writes it (`25¢`, `$1.10`). |
|
|
482
|
-
| `usdText(usd)`, `wholeCents(usd)`, `HATS` | `"$0.25"
|
|
481
|
+
| `PRICE_LADDER`, `rungOf(usd)`, `priceText(usd)` | The six steps nano133.com uses, from 1¢ to $1, and a price as a page writes it (`0.5¢`, `25¢`, `$1.10`). |
|
|
482
|
+
| `usdText(usd)`, `usdDecimals(usd)`, `priceStepOk(usd)`, `PRICE_STEP_USD`, `wholeCents(usd)`, `HATS` | `"$0.25"`, `"$0.005"` (2 to 4 decimals); the same without `$`, as an order keeps it; whether a price has at most 4 decimals (the looks rule since 0.10.0); `0.0001`, the smallest price above 0; whether a price has no fraction of a cent; the hats. |
|
|
483
483
|
| `sameSlots(a, b)`, `cleanSlots(slots)`, `slotValueOk(v)`, `SLOTS_MAX` | Whether two looks have the same slots; a clean copy (none when empty); whether a value is a word or a colour; 8. |
|
|
484
484
|
|
|
485
485
|
| Step | Price | For |
|
|
@@ -491,8 +491,10 @@ The rules (the same as the starter's `AGENTS.md`):
|
|
|
491
491
|
| set | 50¢ | a few pieces that go together |
|
|
492
492
|
| outfit | $1 | a whole look |
|
|
493
493
|
|
|
494
|
-
The ladder is a suggestion. Any price with at most
|
|
495
|
-
|
|
494
|
+
The ladder is a suggestion. Any price with at most 4 decimals, up to $1000, is good: there is no minimum, so a fraction of a cent works ($0.005, $0.0001;
|
|
495
|
+
since 0.10.0, before it whole cents). The shop rounds the Nano amount up to a step of 0.0001 XNO and asks for at least one step, so a creator never gets
|
|
496
|
+
less than the price. `priceStepOk` and `wholeCents` compare a price with its own decimal text (the starter's old check, `Math.round(x * 100) === x * 100`,
|
|
497
|
+
refused $0.07, $0.29, $0.57 and $1.10 because of floating point).
|
|
496
498
|
|
|
497
499
|
## The admin (`/admin`; since 0.2.0)
|
|
498
500
|
|
package/ai/SPEC.md
CHANGED
|
@@ -106,7 +106,7 @@ export const LOOKS = checkLooks(rawLooksJson); // throws with the first mistake
|
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
- Before a change goes live: `[...lookProblems(next), ...removalProblems(current, next, ownedLookIds)]`; show `diffLooks(current, next)`; write the file with `looksJson(next)`.
|
|
109
|
-
- One free look (`priceUsd: 0`) at least. Never change or remove the id of a look that someone owns. Prices in US dollars with at most
|
|
109
|
+
- One free look (`priceUsd: 0`) at least. Never change or remove the id of a look that someone owns. Prices in US dollars with at most 4 decimals (no minimum: 0.005 is half a cent); the server reads the price from the file.
|
|
110
110
|
- A look only changes how the character looks. No random look, no loot box, no look that changes a score.
|
|
111
111
|
- `slots` (optional, at most 8: a name and a word or a `#rrggbb` colour) is where the game keeps its own parts of a look. Read `look.slots` from `checkLooks`. Never change the slots of a look that someone owns: add a new look.
|
|
112
112
|
- `wear` (optional) is the wear slot a look fills: none for an outfit (the character's colours and hat, `change`), or the game's own slot (`"shovel"`, `"charm"`) for an item, which has no `change`. A player wears one look in each slot. Keep one free outfit. Never change the wear slot of a look that someone owns. Read outfits with `isOutfit(look)`.
|
package/dist/admin/amount.d.ts
CHANGED
|
@@ -3,3 +3,7 @@ export declare const RAW_PER_XNO: bigint;
|
|
|
3
3
|
export declare function xnoText(raw: string): string;
|
|
4
4
|
/** A raw amount that can be added: digits only, at most 39 of them. Anything else is not counted (a bad row must not break the summary). */
|
|
5
5
|
export declare const isRawText: (s: unknown) => s is string;
|
|
6
|
+
/** Dollars as plain decimals, as an order keeps them: at least 2 decimals and at most 4, "0.25", "1.00", "0.005", "0.0125". */
|
|
7
|
+
export declare const usdDecimals: (usd: number) => string;
|
|
8
|
+
/** "$0.25", "$1.00", "$0.005". */
|
|
9
|
+
export declare const usdText: (usd: number) => string;
|
package/dist/admin/amount.js
CHANGED
|
@@ -9,3 +9,7 @@ export function xnoText(raw) {
|
|
|
9
9
|
}
|
|
10
10
|
/** A raw amount that can be added: digits only, at most 39 of them. Anything else is not counted (a bad row must not break the summary). */
|
|
11
11
|
export const isRawText = (s) => typeof s === "string" && /^[0-9]{1,39}$/.test(s);
|
|
12
|
+
/** Dollars as plain decimals, as an order keeps them: at least 2 decimals and at most 4, "0.25", "1.00", "0.005", "0.0125". */
|
|
13
|
+
export const usdDecimals = (usd) => usd.toFixed(4).replace(/(\.\d\d\d*?)0+$/, "$1");
|
|
14
|
+
/** "$0.25", "$1.00", "$0.005". */
|
|
15
|
+
export const usdText = (usd) => `$${usdDecimals(usd)}`;
|
|
@@ -15,6 +15,6 @@ export declare function copyKey(value: string, label: string, icon?: Element): H
|
|
|
15
15
|
export declare function ago(ms: number, now?: number): string;
|
|
16
16
|
/** "3 orders", "1 order" */
|
|
17
17
|
export declare const plural: (n: number, word: string) => string;
|
|
18
|
-
/** "$1.25" from cents. */
|
|
18
|
+
/** "$1.25" from cents, "$0.005" from half a cent. */
|
|
19
19
|
export declare const dollars: (cents: number) => string;
|
|
20
20
|
export {};
|
package/dist/admin/panel/dom.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// A tiny helper to build page elements without innerHTML (safe for any text, and fine under a strict CSP). The panel runs in a browser.
|
|
2
|
+
import { usdText } from "../amount.js";
|
|
2
3
|
export function h(tag, attrs = {}, ...children) {
|
|
3
4
|
const el = document.createElement(tag);
|
|
4
5
|
for (const [k, v] of Object.entries(attrs)) {
|
|
@@ -68,5 +69,5 @@ export function ago(ms, now = Date.now()) {
|
|
|
68
69
|
}
|
|
69
70
|
/** "3 orders", "1 order" */
|
|
70
71
|
export const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
71
|
-
/** "$1.25" from cents. */
|
|
72
|
-
export const dollars = (cents) =>
|
|
72
|
+
/** "$1.25" from cents, "$0.005" from half a cent. */
|
|
73
|
+
export const dollars = (cents) => usdText(cents / 100);
|
|
@@ -388,7 +388,7 @@ export function shelfView(first, looks, picture = lookPicture, repoHelp = "To ge
|
|
|
388
388
|
l.id = slugId(l.name, [...draft.filter((x) => x !== l).map((x) => x.id), ...owned()]);
|
|
389
389
|
sync();
|
|
390
390
|
} });
|
|
391
|
-
const priceInput = h("input", { class: "input input-num", type: "number", step: 0.
|
|
391
|
+
const priceInput = h("input", { class: "input input-num", type: "number", step: 0.0001, min: 0, max: 1000, inputmode: "decimal", value: Number.isFinite(l.priceUsd) ? l.priceUsd : "", "aria-label": `Price in US dollars of ${l.name}`, oninput: (e) => {
|
|
392
392
|
const v = e.target.value;
|
|
393
393
|
l.priceUsd = v === "" ? Number.NaN : Number(v);
|
|
394
394
|
sync();
|
|
@@ -20,7 +20,7 @@ export type LookStats = {
|
|
|
20
20
|
/** Orders that were paid after the order ran out (the money is to be sent back; the look was not given). */
|
|
21
21
|
late: number;
|
|
22
22
|
};
|
|
23
|
-
/** "0.25" is 25 cents. */
|
|
23
|
+
/** "0.25" is 25 cents; "0.005" is half a cent (cents keep 2 decimals: a price has at most 4). */
|
|
24
24
|
export declare const centsOf: (usd: string) => number;
|
|
25
25
|
/** The stats for each look id that appears in the orders or in a player's looks. */
|
|
26
26
|
export declare function shelfStats(orders: readonly ShelfOrder[], players: readonly ShelfPlayer[]): Map<string, LookStats>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// The numbers and the words of the Shelf screen in /admin (shelf.ts draws it). Pure: no page, no network. Everything
|
|
2
2
|
// here comes from the game's own database (the admin summary) and from looks.json; nothing is sent anywhere.
|
|
3
|
-
import { ID_PATTERN } from "../../looks/look.js";
|
|
3
|
+
import { ID_PATTERN, usdText } from "../../looks/look.js";
|
|
4
4
|
const none = () => ({ owners: 0, paid: 0, paidCents: 0, late: 0 });
|
|
5
|
-
/** "0.25" is 25 cents. */
|
|
6
|
-
export const centsOf = (usd) => Math.round(Number(usd) *
|
|
5
|
+
/** "0.25" is 25 cents; "0.005" is half a cent (cents keep 2 decimals: a price has at most 4). */
|
|
6
|
+
export const centsOf = (usd) => Math.round(Number(usd) * 10_000) / 100 || 0;
|
|
7
7
|
/** The stats for each look id that appears in the orders or in a player's looks. */
|
|
8
8
|
export function shelfStats(orders, players) {
|
|
9
9
|
const stats = new Map();
|
|
@@ -12,7 +12,7 @@ export function shelfStats(orders, players) {
|
|
|
12
12
|
if (o.status === "paid") {
|
|
13
13
|
const s = of(o.lookId);
|
|
14
14
|
s.paid++;
|
|
15
|
-
s.paidCents
|
|
15
|
+
s.paidCents = Math.round((s.paidCents + centsOf(o.usd)) * 100) / 100;
|
|
16
16
|
}
|
|
17
17
|
else if (o.status === "paid_late")
|
|
18
18
|
of(o.lookId).late++;
|
|
@@ -35,7 +35,7 @@ export function changeText(d) {
|
|
|
35
35
|
export function soldText(s) {
|
|
36
36
|
if (!s || (!s.paid && !s.owners))
|
|
37
37
|
return "Not sold yet";
|
|
38
|
-
return `${s.paid} sold,
|
|
38
|
+
return `${s.paid} sold, ${usdText(s.paidCents / 100)}, ${plural(s.owners, "owner")}`;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* A message to give to an AI assistant that works in the game's repository: the whole new looks.json, what changed, and the rules.
|
|
@@ -57,7 +57,7 @@ export function aiPrompt(text, diff, owned, before) {
|
|
|
57
57
|
diff.changed.length ? `Changed: ${names(diff.changed.map((c) => c.after))}.` : "",
|
|
58
58
|
"",
|
|
59
59
|
`Looks that players own and that must stay in the file with the same id: ${ownedOk.length ? quote(ownedOk) : "none yet"}${odd ? ` (and ${odd} more with an id that is not a valid look id: do not remove any look)` : ""}.`,
|
|
60
|
-
`Rules: keep one free outfit (priceUsd 0, no "wear"), prices in US dollars with at most
|
|
60
|
+
`Rules: keep one free outfit (priceUsd 0, no "wear"), prices in US dollars with at most 4 decimals (0.005 is half a cent), never change or remove the id of a look that someone owns, never change the wear slot or the slots of a look that someone owns (a look with "wear" is an item of that slot, such as a shovel, and has no "change"; slots are words and #rrggbb colours that the game reads, at most 8 on a look), and a look never changes a score or a rule. (The file had ${before.length} looks before.)`,
|
|
61
61
|
"",
|
|
62
62
|
"looks.json:",
|
|
63
63
|
"```json",
|
package/dist/admin/summary.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export type OrderRow = {
|
|
|
14
14
|
hash: string | null;
|
|
15
15
|
};
|
|
16
16
|
export declare const ORDERS_SHOWN = 500;
|
|
17
|
-
/** Money from rows that are all of one status: the count, the XNO text of the raw sum and the dollars (summed in
|
|
17
|
+
/** Money from rows that are all of one status: the count, the XNO text of the raw sum and the dollars (summed in whole $0.0001 steps, so 0.1 + 0.2 is 0.30). */
|
|
18
18
|
export declare function moneyOf(rows: readonly {
|
|
19
19
|
amountRaw: string;
|
|
20
20
|
usd: string;
|
package/dist/admin/summary.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
// The numbers of the summary from the rows of the orders, the same for every adapter: the counts by status, the totals and the shape of an order.
|
|
2
2
|
// An adapter reads its rows; this file turns them into a `StoreSummary`.
|
|
3
|
-
import { isRawText, xnoText } from "./amount.js";
|
|
3
|
+
import { isRawText, usdDecimals, xnoText } from "./amount.js";
|
|
4
4
|
export const ORDERS_SHOWN = 500;
|
|
5
|
-
/**
|
|
6
|
-
const
|
|
5
|
+
/** Steps of $0.0001 from a dollar text like "0.25" or "0.005". Anything that is not plain dollars (a sign, an exponent, hex) counts as 0: a bad row must not change a total. */
|
|
6
|
+
const toSteps = (usd) => {
|
|
7
7
|
if (typeof usd !== "string" || !/^\d{1,9}(\.\d{1,8})?$/.test(usd))
|
|
8
8
|
return 0;
|
|
9
|
-
return Math.round(Number(usd) *
|
|
9
|
+
return Math.round(Number(usd) * 10_000);
|
|
10
10
|
};
|
|
11
|
-
/** Money from rows that are all of one status: the count, the XNO text of the raw sum and the dollars (summed in
|
|
11
|
+
/** Money from rows that are all of one status: the count, the XNO text of the raw sum and the dollars (summed in whole $0.0001 steps, so 0.1 + 0.2 is 0.30). */
|
|
12
12
|
export function moneyOf(rows) {
|
|
13
13
|
let raw = 0n;
|
|
14
|
-
let
|
|
14
|
+
let steps = 0;
|
|
15
15
|
for (const r of rows) {
|
|
16
16
|
if (isRawText(r.amountRaw))
|
|
17
17
|
raw += BigInt(r.amountRaw);
|
|
18
|
-
|
|
18
|
+
steps += toSteps(r.usd);
|
|
19
19
|
}
|
|
20
|
-
return { count: rows.length, xno: xnoText(raw.toString()), usd: (
|
|
20
|
+
return { count: rows.length, xno: xnoText(raw.toString()), usd: usdDecimals(steps / 10_000) };
|
|
21
21
|
}
|
|
22
22
|
/** Where an order counts: an open order past its expiry counts as expired. */
|
|
23
23
|
export const statusOf = (status, expiresAt, now) => (status === "open" && expiresAt < now ? "expired" : status);
|
package/dist/looks/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { HATS, ID_PATTERN, COLOR_PATTERN, NAME_MAX, OUTFIT, PRICE_MAX_USD, SLOTS_MAX, SLOT_NAME_PATTERN, SLOT_WORD_PATTERN, RESERVED_SLOT_NAMES, wholeCents, lookProblems, checkLooks, cleanSlots, slotValueOk, usdText, wearOf, isOutfit } from "./look.js";
|
|
1
|
+
export { HATS, ID_PATTERN, COLOR_PATTERN, NAME_MAX, OUTFIT, PRICE_MAX_USD, PRICE_STEP_USD, SLOTS_MAX, SLOT_NAME_PATTERN, SLOT_WORD_PATTERN, RESERVED_SLOT_NAMES, wholeCents, priceStepOk, lookProblems, checkLooks, cleanSlots, slotValueOk, usdDecimals, usdText, wearOf, isOutfit } from "./look.js";
|
|
2
2
|
export type { Change, Hat, Look, Problem, Slots } from "./look.js";
|
|
3
3
|
export { PRICE_LADDER, rungUsd, rungOf, priceText } from "./ladder.js";
|
|
4
4
|
export type { Rung } from "./ladder.js";
|
package/dist/looks/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { HATS, ID_PATTERN, COLOR_PATTERN, NAME_MAX, OUTFIT, PRICE_MAX_USD, SLOTS_MAX, SLOT_NAME_PATTERN, SLOT_WORD_PATTERN, RESERVED_SLOT_NAMES, wholeCents, lookProblems, checkLooks, cleanSlots, slotValueOk, usdText, wearOf, isOutfit } from "./look.js";
|
|
1
|
+
export { HATS, ID_PATTERN, COLOR_PATTERN, NAME_MAX, OUTFIT, PRICE_MAX_USD, PRICE_STEP_USD, SLOTS_MAX, SLOT_NAME_PATTERN, SLOT_WORD_PATTERN, RESERVED_SLOT_NAMES, wholeCents, priceStepOk, lookProblems, checkLooks, cleanSlots, slotValueOk, usdDecimals, usdText, wearOf, isOutfit } from "./look.js";
|
|
2
2
|
export { PRICE_LADDER, rungUsd, rungOf, priceText } from "./ladder.js";
|
|
3
3
|
export { removalProblems, diffLooks, sameSlots } from "./change.js";
|
|
4
4
|
export { looksJson, slugId } from "./json.js";
|
package/dist/looks/ladder.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ export declare const PRICE_LADDER: readonly Rung[];
|
|
|
10
10
|
export declare const rungUsd: (r: Rung) => number;
|
|
11
11
|
/** The rung that has exactly this price, or undefined. */
|
|
12
12
|
export declare function rungOf(usd: number): Rung | undefined;
|
|
13
|
-
/** A price as a page writes it: 1¢, 25¢, $1, $1.10. */
|
|
13
|
+
/** A price as a page writes it: 0.5¢, 1¢, 25¢, $1, $1.10, $1.005. */
|
|
14
14
|
export declare function priceText(usd: number): string;
|
package/dist/looks/ladder.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The price ladder: the six steps from a cent to a dollar that nano133.com uses for its own cosmetics and shows on /build/cosmetics.
|
|
2
|
-
// It is a suggestion. A look can have any price with at most
|
|
2
|
+
// It is a suggestion. A look can have any price with at most 4 decimals ($0.0001 steps), up to $1000.
|
|
3
|
+
import { usdText } from "./look.js";
|
|
3
4
|
export const PRICE_LADDER = [
|
|
4
5
|
{ name: "dye", cents: 1, for: "a colour" },
|
|
5
6
|
{ name: "charm", cents: 2, for: "a small mark or a badge" },
|
|
@@ -14,11 +15,10 @@ export const rungUsd = (r) => r.cents / 100;
|
|
|
14
15
|
export function rungOf(usd) {
|
|
15
16
|
return PRICE_LADDER.find((r) => Math.abs(rungUsd(r) - usd) < 1e-9);
|
|
16
17
|
}
|
|
17
|
-
/** A price as a page writes it: 1¢, 25¢, $1, $1.10. */
|
|
18
|
+
/** A price as a page writes it: 0.5¢, 1¢, 25¢, $1, $1.10, $1.005. */
|
|
18
19
|
export function priceText(usd) {
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
return `${
|
|
22
|
-
|
|
23
|
-
return Number.isInteger(dollars) ? `$${dollars}` : `$${dollars.toFixed(2)}`;
|
|
20
|
+
const steps = Math.round(usd * 10_000);
|
|
21
|
+
if (steps < 10_000)
|
|
22
|
+
return `${steps / 100}¢`;
|
|
23
|
+
return steps % 10_000 === 0 ? `$${steps / 10_000}` : usdText(steps / 10_000);
|
|
24
24
|
}
|
package/dist/looks/look.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type Look = {
|
|
|
12
12
|
id: string;
|
|
13
13
|
/** What the player reads, 1 to 40 characters. */
|
|
14
14
|
name: string;
|
|
15
|
-
/** The price in US dollars with at most
|
|
15
|
+
/** The price in US dollars with at most 4 decimals, for example 0.25 or 0.005. 0 means free for everyone. */
|
|
16
16
|
priceUsd: number;
|
|
17
17
|
/**
|
|
18
18
|
* The wear slot that the look fills: none (or "outfit") for an outfit, or a slot of the game's own ("shovel", "charm"): 1 to 24 lowercase letters, digits
|
|
@@ -61,6 +61,10 @@ export type Problem = {
|
|
|
61
61
|
* and 1e-9 would pass): a price is whole cents when it is exactly the number that its own two-decimal text names.
|
|
62
62
|
*/
|
|
63
63
|
export declare function wholeCents(usd: number): boolean;
|
|
64
|
+
/** The smallest step of a price: $0.0001, a hundredth of a cent. A price above 0 is at least this, and the shop asks for at least one 0.0001 XNO step. */
|
|
65
|
+
export declare const PRICE_STEP_USD = 0.0001;
|
|
66
|
+
/** Whether a price has at most 4 decimals (whole steps of $0.0001), by the same rule as `wholeCents`: it is exactly the number its own four-decimal text names. */
|
|
67
|
+
export declare function priceStepOk(usd: number): boolean;
|
|
64
68
|
/** Every mistake in a list of looks (looks.json as it was read), in the order of the list. An empty array means the list is good. */
|
|
65
69
|
export declare function lookProblems(raw: unknown): Problem[];
|
|
66
70
|
/** Whether a slot's value is good: a word or a colour. */
|
|
@@ -73,7 +77,8 @@ export declare function cleanSlots(slots: Slots | undefined): Slots | undefined;
|
|
|
73
77
|
/** The looks, or an Error that says the first mistake in plain words. The same words as the starter's `checkLooks`. */
|
|
74
78
|
export declare function checkLooks(rawInput: unknown): Look[];
|
|
75
79
|
/** "$0.25" */
|
|
76
|
-
|
|
80
|
+
/** "$0.25", "$1.00", "$0.005"; and the same without "$", as an order keeps it ("0.005"). The money code's own helpers. */
|
|
81
|
+
export { usdDecimals, usdText } from "../admin/amount.js";
|
|
77
82
|
/** The wear slot that a look fills: OUTFIT for an outfit (no `wear`), or the game's own slot. */
|
|
78
83
|
export declare const wearOf: (l: Pick<Look, "wear">) => string;
|
|
79
84
|
/** Whether a look is an outfit (the character's own look, with its `change`), not an item of another wear slot. */
|
package/dist/looks/look.js
CHANGED
|
@@ -27,6 +27,12 @@ export const PRICE_MAX_USD = 1000;
|
|
|
27
27
|
export function wholeCents(usd) {
|
|
28
28
|
return typeof usd === "number" && Number.isFinite(usd) && Number(usd.toFixed(2)) === usd;
|
|
29
29
|
}
|
|
30
|
+
/** The smallest step of a price: $0.0001, a hundredth of a cent. A price above 0 is at least this, and the shop asks for at least one 0.0001 XNO step. */
|
|
31
|
+
export const PRICE_STEP_USD = 0.0001;
|
|
32
|
+
/** Whether a price has at most 4 decimals (whole steps of $0.0001), by the same rule as `wholeCents`: it is exactly the number its own four-decimal text names. */
|
|
33
|
+
export function priceStepOk(usd) {
|
|
34
|
+
return typeof usd === "number" && Number.isFinite(usd) && Number(usd.toFixed(4)) === usd;
|
|
35
|
+
}
|
|
30
36
|
/** Every mistake in a list of looks (looks.json as it was read), in the order of the list. An empty array means the list is good. */
|
|
31
37
|
export function lookProblems(raw) {
|
|
32
38
|
const out = [];
|
|
@@ -48,8 +54,8 @@ export function lookProblems(raw) {
|
|
|
48
54
|
add("name", '"name" must be 1 to 40 characters');
|
|
49
55
|
if (typeof v.priceUsd !== "number" || !Number.isFinite(v.priceUsd) || v.priceUsd < 0 || v.priceUsd > PRICE_MAX_USD)
|
|
50
56
|
add("priceUsd", '"priceUsd" must be a number from 0 to 1000');
|
|
51
|
-
else if (!
|
|
52
|
-
add("priceUsd", '"priceUsd" can have at most
|
|
57
|
+
else if (!priceStepOk(v.priceUsd))
|
|
58
|
+
add("priceUsd", '"priceUsd" can have at most 4 decimals (0.0001 is the smallest price above 0)');
|
|
53
59
|
const wearOk = v.wear === undefined || (typeof v.wear === "string" && SLOT_NAME_PATTERN.test(v.wear) && !RESERVED_SLOT_NAMES.includes(v.wear));
|
|
54
60
|
if (!wearOk)
|
|
55
61
|
add("wear", `"wear" must be "${OUTFIT}" or the name of a wear slot: 1 to 24 lowercase letters, digits or dashes, starting with a letter ("shovel")`);
|
|
@@ -131,7 +137,8 @@ export function checkLooks(rawInput) {
|
|
|
131
137
|
});
|
|
132
138
|
}
|
|
133
139
|
/** "$0.25" */
|
|
134
|
-
|
|
140
|
+
/** "$0.25", "$1.00", "$0.005"; and the same without "$", as an order keeps it ("0.005"). The money code's own helpers. */
|
|
141
|
+
export { usdDecimals, usdText } from "../admin/amount.js";
|
|
135
142
|
/** The wear slot that a look fills: OUTFIT for an outfit (no `wear`), or the game's own slot. */
|
|
136
143
|
export const wearOf = (l) => l.wear ?? OUTFIT;
|
|
137
144
|
/** Whether a look is an outfit (the character's own look, with its `change`), not an item of another wear slot. */
|
package/dist/shop/handler.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// TEST MODE: with no payee (none, the example address, or a wrong checksum) or `testMode: true`, the answer carries NO address and NO link, and the page
|
|
15
15
|
// shows "TEST MODE: DO NOT PAY". A payment to an address nobody holds the key for is lost for good.
|
|
16
16
|
import { fail, json, memoryLimiter, readLimited, sameOrigin } from "../admin/http.js";
|
|
17
|
-
import { checkLooks } from "../looks/look.js";
|
|
17
|
+
import { checkLooks, usdDecimals } from "../looks/look.js";
|
|
18
18
|
import { payeeOf } from "./address.js";
|
|
19
19
|
import { nanoUri, priceRaw, xnoText } from "./amount.js";
|
|
20
20
|
import { PUBLIC_NODES } from "./nodes.js";
|
|
@@ -101,7 +101,7 @@ export function createShopHandler(options) {
|
|
|
101
101
|
if (!price)
|
|
102
102
|
return fail(503, "Prices are not available now. Try again later.");
|
|
103
103
|
try {
|
|
104
|
-
const order = await reserveOrder(store, { player, lookId: look.id, usd: look.priceUsd
|
|
104
|
+
const order = await reserveOrder(store, { player, lookId: look.id, usd: usdDecimals(look.priceUsd), rate: rate.usd, price, visitor, now: at });
|
|
105
105
|
return json(payView(order, at), 201);
|
|
106
106
|
}
|
|
107
107
|
catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nano133/nanoterra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Nanoterra for a creator's game, in one package: the player's sign-in ticket, the player's character (a toon body on the island's skeleton), the looks ladder, the admin, the friends list, an options menu, emotes, the player's sign-in (a passkey or a wallet app) and a shop that sells looks for Nano with no key on the server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/src/admin/amount.ts
CHANGED
|
@@ -12,3 +12,9 @@ export function xnoText(raw: string): string {
|
|
|
12
12
|
|
|
13
13
|
/** A raw amount that can be added: digits only, at most 39 of them. Anything else is not counted (a bad row must not break the summary). */
|
|
14
14
|
export const isRawText = (s: unknown): s is string => typeof s === "string" && /^[0-9]{1,39}$/.test(s);
|
|
15
|
+
|
|
16
|
+
/** Dollars as plain decimals, as an order keeps them: at least 2 decimals and at most 4, "0.25", "1.00", "0.005", "0.0125". */
|
|
17
|
+
export const usdDecimals = (usd: number): string => usd.toFixed(4).replace(/(\.\d\d\d*?)0+$/, "$1");
|
|
18
|
+
|
|
19
|
+
/** "$0.25", "$1.00", "$0.005". */
|
|
20
|
+
export const usdText = (usd: number): string => `$${usdDecimals(usd)}`;
|
package/src/admin/panel/dom.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// A tiny helper to build page elements without innerHTML (safe for any text, and fine under a strict CSP). The panel runs in a browser.
|
|
2
2
|
|
|
3
|
+
import { usdText } from "../amount.js";
|
|
4
|
+
|
|
3
5
|
type Attrs = Record<string, string | number | boolean | undefined | ((e: Event) => void)>;
|
|
4
6
|
type Child = Node | string | null | undefined | false;
|
|
5
7
|
|
|
@@ -67,5 +69,5 @@ export function ago(ms: number, now = Date.now()): string {
|
|
|
67
69
|
/** "3 orders", "1 order" */
|
|
68
70
|
export const plural = (n: number, word: string): string => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
69
71
|
|
|
70
|
-
/** "$1.25" from cents. */
|
|
71
|
-
export const dollars = (cents: number): string =>
|
|
72
|
+
/** "$1.25" from cents, "$0.005" from half a cent. */
|
|
73
|
+
export const dollars = (cents: number): string => usdText(cents / 100);
|
package/src/admin/panel/shelf.ts
CHANGED
|
@@ -503,7 +503,7 @@ export function shelfView(first: ShelfData, looks: readonly Look[], picture: (l:
|
|
|
503
503
|
sync();
|
|
504
504
|
} });
|
|
505
505
|
|
|
506
|
-
const priceInput = h("input", { class: "input input-num", type: "number", step: 0.
|
|
506
|
+
const priceInput = h("input", { class: "input input-num", type: "number", step: 0.0001, min: 0, max: 1000, inputmode: "decimal", value: Number.isFinite(l.priceUsd) ? l.priceUsd : "", "aria-label": `Price in US dollars of ${l.name}`, oninput: (e: Event) => {
|
|
507
507
|
const v = (e.target as HTMLInputElement).value;
|
|
508
508
|
l.priceUsd = v === "" ? Number.NaN : Number(v);
|
|
509
509
|
sync();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// here comes from the game's own database (the admin summary) and from looks.json; nothing is sent anywhere.
|
|
3
3
|
|
|
4
4
|
import type { LooksDiff } from "../../looks/change.js";
|
|
5
|
-
import { ID_PATTERN, type Look } from "../../looks/look.js";
|
|
5
|
+
import { ID_PATTERN, usdText, type Look } from "../../looks/look.js";
|
|
6
6
|
|
|
7
7
|
export type ShelfOrder = { lookId: string; status: string; usd: string };
|
|
8
8
|
export type ShelfPlayer = { looks: { id: string }[] };
|
|
@@ -20,8 +20,8 @@ export type LookStats = {
|
|
|
20
20
|
|
|
21
21
|
const none = (): LookStats => ({ owners: 0, paid: 0, paidCents: 0, late: 0 });
|
|
22
22
|
|
|
23
|
-
/** "0.25" is 25 cents. */
|
|
24
|
-
export const centsOf = (usd: string): number => Math.round(Number(usd) *
|
|
23
|
+
/** "0.25" is 25 cents; "0.005" is half a cent (cents keep 2 decimals: a price has at most 4). */
|
|
24
|
+
export const centsOf = (usd: string): number => Math.round(Number(usd) * 10_000) / 100 || 0;
|
|
25
25
|
|
|
26
26
|
/** The stats for each look id that appears in the orders or in a player's looks. */
|
|
27
27
|
export function shelfStats(orders: readonly ShelfOrder[], players: readonly ShelfPlayer[]): Map<string, LookStats> {
|
|
@@ -31,7 +31,7 @@ export function shelfStats(orders: readonly ShelfOrder[], players: readonly Shel
|
|
|
31
31
|
if (o.status === "paid") {
|
|
32
32
|
const s = of(o.lookId);
|
|
33
33
|
s.paid++;
|
|
34
|
-
s.paidCents
|
|
34
|
+
s.paidCents = Math.round((s.paidCents + centsOf(o.usd)) * 100) / 100;
|
|
35
35
|
} else if (o.status === "paid_late") of(o.lookId).late++;
|
|
36
36
|
}
|
|
37
37
|
for (const p of players) for (const l of new Set(p.looks.map((x) => x.id))) of(l).owners++;
|
|
@@ -53,7 +53,7 @@ export function changeText(d: LooksDiff): string {
|
|
|
53
53
|
/** What to say when a player's money is in a look: "3 sold, $0.75, 3 owners", or "Not sold yet". */
|
|
54
54
|
export function soldText(s: LookStats | undefined): string {
|
|
55
55
|
if (!s || (!s.paid && !s.owners)) return "Not sold yet";
|
|
56
|
-
return `${s.paid} sold,
|
|
56
|
+
return `${s.paid} sold, ${usdText(s.paidCents / 100)}, ${plural(s.owners, "owner")}`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
@@ -76,7 +76,7 @@ export function aiPrompt(text: string, diff: LooksDiff, owned: readonly string[]
|
|
|
76
76
|
diff.changed.length ? `Changed: ${names(diff.changed.map((c) => c.after))}.` : "",
|
|
77
77
|
"",
|
|
78
78
|
`Looks that players own and that must stay in the file with the same id: ${ownedOk.length ? quote(ownedOk) : "none yet"}${odd ? ` (and ${odd} more with an id that is not a valid look id: do not remove any look)` : ""}.`,
|
|
79
|
-
`Rules: keep one free outfit (priceUsd 0, no "wear"), prices in US dollars with at most
|
|
79
|
+
`Rules: keep one free outfit (priceUsd 0, no "wear"), prices in US dollars with at most 4 decimals (0.005 is half a cent), never change or remove the id of a look that someone owns, never change the wear slot or the slots of a look that someone owns (a look with "wear" is an item of that slot, such as a shovel, and has no "change"; slots are words and #rrggbb colours that the game reads, at most 8 on a look), and a look never changes a score or a rule. (The file had ${before.length} looks before.)`,
|
|
80
80
|
"",
|
|
81
81
|
"looks.json:",
|
|
82
82
|
"```json",
|
package/src/admin/summary.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The numbers of the summary from the rows of the orders, the same for every adapter: the counts by status, the totals and the shape of an order.
|
|
2
2
|
// An adapter reads its rows; this file turns them into a `StoreSummary`.
|
|
3
3
|
|
|
4
|
-
import { isRawText, xnoText } from "./amount.js";
|
|
4
|
+
import { isRawText, usdDecimals, xnoText } from "./amount.js";
|
|
5
5
|
import type { AdminOrder, Money, StoreSummary } from "./store.js";
|
|
6
6
|
|
|
7
7
|
/** An order as an adapter reads it: the amount is the raw amount (digits), the times are ms. */
|
|
@@ -21,21 +21,21 @@ export type OrderRow = {
|
|
|
21
21
|
|
|
22
22
|
export const ORDERS_SHOWN = 500;
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
const
|
|
24
|
+
/** Steps of $0.0001 from a dollar text like "0.25" or "0.005". Anything that is not plain dollars (a sign, an exponent, hex) counts as 0: a bad row must not change a total. */
|
|
25
|
+
const toSteps = (usd: string): number => {
|
|
26
26
|
if (typeof usd !== "string" || !/^\d{1,9}(\.\d{1,8})?$/.test(usd)) return 0;
|
|
27
|
-
return Math.round(Number(usd) *
|
|
27
|
+
return Math.round(Number(usd) * 10_000);
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
/** Money from rows that are all of one status: the count, the XNO text of the raw sum and the dollars (summed in
|
|
30
|
+
/** Money from rows that are all of one status: the count, the XNO text of the raw sum and the dollars (summed in whole $0.0001 steps, so 0.1 + 0.2 is 0.30). */
|
|
31
31
|
export function moneyOf(rows: readonly { amountRaw: string; usd: string }[]): Money {
|
|
32
32
|
let raw = 0n;
|
|
33
|
-
let
|
|
33
|
+
let steps = 0;
|
|
34
34
|
for (const r of rows) {
|
|
35
35
|
if (isRawText(r.amountRaw)) raw += BigInt(r.amountRaw);
|
|
36
|
-
|
|
36
|
+
steps += toSteps(r.usd);
|
|
37
37
|
}
|
|
38
|
-
return { count: rows.length, xno: xnoText(raw.toString()), usd: (
|
|
38
|
+
return { count: rows.length, xno: xnoText(raw.toString()), usd: usdDecimals(steps / 10_000) };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** Where an order counts: an open order past its expiry counts as expired. */
|
package/src/looks/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { HATS, ID_PATTERN, COLOR_PATTERN, NAME_MAX, OUTFIT, PRICE_MAX_USD, SLOTS_MAX, SLOT_NAME_PATTERN, SLOT_WORD_PATTERN, RESERVED_SLOT_NAMES, wholeCents, lookProblems, checkLooks, cleanSlots, slotValueOk, usdText, wearOf, isOutfit } from "./look.js";
|
|
1
|
+
export { HATS, ID_PATTERN, COLOR_PATTERN, NAME_MAX, OUTFIT, PRICE_MAX_USD, PRICE_STEP_USD, SLOTS_MAX, SLOT_NAME_PATTERN, SLOT_WORD_PATTERN, RESERVED_SLOT_NAMES, wholeCents, priceStepOk, lookProblems, checkLooks, cleanSlots, slotValueOk, usdDecimals, usdText, wearOf, isOutfit } from "./look.js";
|
|
2
2
|
export type { Change, Hat, Look, Problem, Slots } from "./look.js";
|
|
3
3
|
export { PRICE_LADDER, rungUsd, rungOf, priceText } from "./ladder.js";
|
|
4
4
|
export type { Rung } from "./ladder.js";
|
package/src/looks/ladder.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// The price ladder: the six steps from a cent to a dollar that nano133.com uses for its own cosmetics and shows on /build/cosmetics.
|
|
2
|
-
// It is a suggestion. A look can have any price with at most
|
|
2
|
+
// It is a suggestion. A look can have any price with at most 4 decimals ($0.0001 steps), up to $1000.
|
|
3
|
+
|
|
4
|
+
import { usdText } from "./look.js";
|
|
3
5
|
|
|
4
6
|
export type Rung = {
|
|
5
7
|
name: "dye" | "charm" | "piece" | "statement" | "set" | "outfit";
|
|
@@ -26,10 +28,9 @@ export function rungOf(usd: number): Rung | undefined {
|
|
|
26
28
|
return PRICE_LADDER.find((r) => Math.abs(rungUsd(r) - usd) < 1e-9);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
/** A price as a page writes it: 1¢, 25¢, $1, $1.10. */
|
|
31
|
+
/** A price as a page writes it: 0.5¢, 1¢, 25¢, $1, $1.10, $1.005. */
|
|
30
32
|
export function priceText(usd: number): string {
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
return Number.isInteger(dollars) ? `$${dollars}` : `$${dollars.toFixed(2)}`;
|
|
33
|
+
const steps = Math.round(usd * 10_000);
|
|
34
|
+
if (steps < 10_000) return `${steps / 100}¢`;
|
|
35
|
+
return steps % 10_000 === 0 ? `$${steps / 10_000}` : usdText(steps / 10_000);
|
|
35
36
|
}
|
package/src/looks/look.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type Look = {
|
|
|
17
17
|
id: string;
|
|
18
18
|
/** What the player reads, 1 to 40 characters. */
|
|
19
19
|
name: string;
|
|
20
|
-
/** The price in US dollars with at most
|
|
20
|
+
/** The price in US dollars with at most 4 decimals, for example 0.25 or 0.005. 0 means free for everyone. */
|
|
21
21
|
priceUsd: number;
|
|
22
22
|
/**
|
|
23
23
|
* The wear slot that the look fills: none (or "outfit") for an outfit, or a slot of the game's own ("shovel", "charm"): 1 to 24 lowercase letters, digits
|
|
@@ -73,6 +73,14 @@ export function wholeCents(usd: number): boolean {
|
|
|
73
73
|
return typeof usd === "number" && Number.isFinite(usd) && Number(usd.toFixed(2)) === usd;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/** The smallest step of a price: $0.0001, a hundredth of a cent. A price above 0 is at least this, and the shop asks for at least one 0.0001 XNO step. */
|
|
77
|
+
export const PRICE_STEP_USD = 0.0001;
|
|
78
|
+
|
|
79
|
+
/** Whether a price has at most 4 decimals (whole steps of $0.0001), by the same rule as `wholeCents`: it is exactly the number its own four-decimal text names. */
|
|
80
|
+
export function priceStepOk(usd: number): boolean {
|
|
81
|
+
return typeof usd === "number" && Number.isFinite(usd) && Number(usd.toFixed(4)) === usd;
|
|
82
|
+
}
|
|
83
|
+
|
|
76
84
|
/** Every mistake in a list of looks (looks.json as it was read), in the order of the list. An empty array means the list is good. */
|
|
77
85
|
export function lookProblems(raw: unknown): Problem[] {
|
|
78
86
|
const out: Problem[] = [];
|
|
@@ -88,7 +96,7 @@ export function lookProblems(raw: unknown): Problem[] {
|
|
|
88
96
|
else seen.add(v.id);
|
|
89
97
|
if (typeof v.name !== "string" || !v.name.trim() || v.name.length > NAME_MAX) add("name", '"name" must be 1 to 40 characters');
|
|
90
98
|
if (typeof v.priceUsd !== "number" || !Number.isFinite(v.priceUsd) || v.priceUsd < 0 || v.priceUsd > PRICE_MAX_USD) add("priceUsd", '"priceUsd" must be a number from 0 to 1000');
|
|
91
|
-
else if (!
|
|
99
|
+
else if (!priceStepOk(v.priceUsd)) add("priceUsd", '"priceUsd" can have at most 4 decimals (0.0001 is the smallest price above 0)');
|
|
92
100
|
const wearOk = v.wear === undefined || (typeof v.wear === "string" && SLOT_NAME_PATTERN.test(v.wear) && !RESERVED_SLOT_NAMES.includes(v.wear));
|
|
93
101
|
if (!wearOk) add("wear", `"wear" must be "${OUTFIT}" or the name of a wear slot: 1 to 24 lowercase letters, digits or dashes, starting with a letter ("shovel")`);
|
|
94
102
|
const outfit = v.wear === undefined || v.wear === OUTFIT;
|
|
@@ -160,7 +168,8 @@ export function checkLooks(rawInput: unknown): Look[] {
|
|
|
160
168
|
}
|
|
161
169
|
|
|
162
170
|
/** "$0.25" */
|
|
163
|
-
|
|
171
|
+
/** "$0.25", "$1.00", "$0.005"; and the same without "$", as an order keeps it ("0.005"). The money code's own helpers. */
|
|
172
|
+
export { usdDecimals, usdText } from "../admin/amount.js";
|
|
164
173
|
|
|
165
174
|
/** The wear slot that a look fills: OUTFIT for an outfit (no `wear`), or the game's own slot. */
|
|
166
175
|
export const wearOf = (l: Pick<Look, "wear">): string => l.wear ?? OUTFIT;
|
package/src/shop/handler.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// shows "TEST MODE: DO NOT PAY". A payment to an address nobody holds the key for is lost for good.
|
|
16
16
|
|
|
17
17
|
import { fail, json, memoryLimiter, readLimited, sameOrigin } from "../admin/http.js";
|
|
18
|
-
import { checkLooks, type Look } from "../looks/look.js";
|
|
18
|
+
import { checkLooks, usdDecimals, type Look } from "../looks/look.js";
|
|
19
19
|
import { payeeOf } from "./address.js";
|
|
20
20
|
import { nanoUri, priceRaw, xnoText } from "./amount.js";
|
|
21
21
|
import { PUBLIC_NODES, type Fetch, type NanoNode, type RpcOptions } from "./nodes.js";
|
|
@@ -157,7 +157,7 @@ export function createShopHandler(options: ShopHandlerOptions): ShopHandler {
|
|
|
157
157
|
const price = priceRaw(look.priceUsd, rate.usd);
|
|
158
158
|
if (!price) return fail(503, "Prices are not available now. Try again later.");
|
|
159
159
|
try {
|
|
160
|
-
const order = await reserveOrder(store, { player, lookId: look.id, usd: look.priceUsd
|
|
160
|
+
const order = await reserveOrder(store, { player, lookId: look.id, usd: usdDecimals(look.priceUsd), rate: rate.usd, price, visitor, now: at });
|
|
161
161
|
return json(payView(order, at), 201);
|
|
162
162
|
} catch (e) {
|
|
163
163
|
if (e instanceof ShopBusy) return fail(503, "Many players are paying this price right now. Try again in a minute.");
|