From cc930a114f2c01da7c5aabbcc04f1d7a0fc5a0a1 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Fri, 25 Sep 2026 07:45:29 +0300 Subject: [PATCH] feat(packages): ludic.gear - kinds of kit with tiers bought flat through a purse port once the standing is there, the kind's item following, per-tier values and mults, stack limits, the hand and charges that burn by the game hour, facts for a tier bought and a charge run out, a save section by key Co-Authored-By: Claude Opus 5.5 --- packages/ludic.gear/README.md | 58 ++++++++ packages/ludic.gear/buy.ludic | 25 ++++ packages/ludic.gear/defs.ludic | 55 ++++++++ packages/ludic.gear/hand.ludic | 72 ++++++++++ packages/ludic.gear/index.ludic | 12 ++ packages/ludic.gear/package.ludic | 6 + packages/ludic.gear/ports.ludic | 20 +++ packages/ludic.gear/state.ludic | 37 +++++ packages/ludic.gear/system.ludic | 49 +++++++ packages/ludic.gear/tests/gear_test.ludic | 163 ++++++++++++++++++++++ packages/ludic.gear/tiers.ludic | 72 ++++++++++ 11 files changed, 569 insertions(+) create mode 100644 packages/ludic.gear/README.md create mode 100644 packages/ludic.gear/buy.ludic create mode 100644 packages/ludic.gear/defs.ludic create mode 100644 packages/ludic.gear/hand.ludic create mode 100644 packages/ludic.gear/index.ludic create mode 100644 packages/ludic.gear/package.ludic create mode 100644 packages/ludic.gear/ports.ludic create mode 100644 packages/ludic.gear/state.ludic create mode 100644 packages/ludic.gear/system.ludic create mode 100644 packages/ludic.gear/tests/gear_test.ludic create mode 100644 packages/ludic.gear/tiers.ludic diff --git a/packages/ludic.gear/README.md b/packages/ludic.gear/README.md new file mode 100644 index 00000000..778e188f --- /dev/null +++ b/packages/ludic.gear/README.md @@ -0,0 +1,58 @@ +# ludic.gear + +Tiers of kit bought one at a time with money once a vendor knows you, the numbers each tier is +worth, what is in the hand, and the charges that burn down while in use - a torch's flame, a +battery. Uses [`ludic.base`](../ludic.base/README.md) and nothing else. + +```ludic +import "ludic.gear" +``` + +The game fills three open registries, usually from resource files: `def GearKinds from +"gear.lres"`, `def GearValues from "gear_values.lres"`, `def GearCharges from "gear_charges.lres"`. +A kind's names, blurbs and vendor, and what running out of a charge means, are the game's. + +## The rules it keeps + +1. **A tier is bought flat.** `gear_buy(k)` wants the next tier's `rep` in `GearPurse.rep()` and its + `cost` in `GearPurse.money()`, spends it through `GearPurse.spend`, and says `GEAR_BOUGHT`. + Nothing about a tier's price moves with a shop's curve or its week. +2. **The kind's item follows, and an item carried counts.** A kind with an `item` puts one in the + pack when bought and none is there, and `gear_sync()` (after every load) lifts a kind to its + first tier when the pack already carries the item. +3. **A tier decides numbers by table.** `gear_value(v)` is `GearValues[v].at[level]`, the last entry + holding past the end; `gear_mult(k)` is the kind's own `mult` the same way (1.0 when it has + none). `gear_stack_limit(base, mult)` keeps a single thing single and never scales a stack to + nothing. +4. **A charge burns only while it runs, by game hours.** A `hand` charge runs only while its item + is held, and a new thing in the hand puts it out. One that reaches zero stops and says + `GEAR_RAN_OUT`; replacing a burnt torch is the game's. + +## The ports + +```ludic +export port GearPurse { money: fn() -> int, spend: fn(int) -> bool, rep: fn() -> int } # required +export port GearPack { count: fn(int) -> int, add: fn(int, int) -> int } # unbound: empty, takes nothing +export port GearWorld { hidden: fn(int) -> bool } # unbound: every kind on sale +``` + +## API + +| | | +| --- | --- | +| `GearKind { key, vendor, icon, tiers, blurb, howto, names, cost, rep, item, start, mult }`, `open registry GearKinds ... as GEAR` | a kind: per tier to buy, `names` / `cost` / `rep`; `start` the level a trip begins at | +| `GearValue { key, gear, at }`, `open registry GearValues ... as GV` | a number a kind's tier decides, per level from 0 | +| `GearCharge { key, item, full, per_hour, rate, hand }`, `open registry GearCharges ... as CHARGE` | a charge; `rate` a value's index to burn by instead of `per_hour` | +| `gear_level(k)`, `gear_set_level(k, lv)`, `gear_owned(k)`, `gear_best(k)`, `gear_level_name(k, lv)` | what is owned | +| `gear_next_name(k)`, `gear_next_cost(k)`, `gear_next_rep(k)`, `gear_can_buy(k)`, `gear_buy(k)`, `gear_kinds_at(vendor)`, `gear_sync()` | buying | +| `gear_mult(k)`, `gear_value(v)`, `gear_stack_limit(base, mult)` | what a tier is worth | +| `gear_hand()`, `gear_set_hand(it)`, `gear_lit()` | the hand | +| `gear_charge_of(it)`, `gear_charge(c)`, `gear_charge_set(c, v)`, `gear_charge_fill(c)`, `gear_running(c)`, `gear_run(c, on)` | the charges | +| `gear_facts() -> Queue` | `{ what: GEAR_BOUGHT / GEAR_RAN_OUT, kind, level, cost, charge, item }` | +| `gear_reset()`, `gear_save()`, `gear_load(v, version)`, `gear_system()` | `"gear"`, `PH_SIMULATE`: the burn each tick; the levels and charges saved by key (the hand is not) | + +## Tests + +```bash +ludic test packages/ludic.gear +``` diff --git a/packages/ludic.gear/buy.ludic b/packages/ludic.gear/buy.ludic new file mode 100644 index 00000000..de05c837 --- /dev/null +++ b/packages/ludic.gear/buy.ludic @@ -0,0 +1,25 @@ +# buy.ludic - a tier bought with money once the standing is there; the kind's item follows +export function gear_can_buy(k: int) -> bool { + if not gear_ok(k) or gear_best(k) or GearWorld.hidden(k) { return false } + return GearPurse.rep() >= gear_next_rep(k) and GearPurse.money() >= gear_next_cost(k) +} + +export function gear_buy(k: int) -> bool { + if not gear_can_buy(k) { return false } + let cost = gear_next_cost(k) + if not GearPurse.spend(cost) { return false } + gear__lv[k] += 1 + let it = GearKinds[k].item + if it >= 0 and GearPack.count(it) == 0 { GearPack.add(it, 1) } + gear__fact(GEAR_BOUGHT, k, -1, cost) + return true +} + +# a kind whose item is already carried counts as its first tier +export function gear_sync() -> void { + gear__ensure() + for k in 0 .. GEAR_COUNT { + let it = GearKinds[k].item + if it >= 0 and gear__lv[k] == 0 and GearPack.count(it) > 0 { gear__lv[k] = 1 } + } +} diff --git a/packages/ludic.gear/defs.ludic b/packages/ludic.gear/defs.ludic new file mode 100644 index 00000000..fbd3f500 --- /dev/null +++ b/packages/ludic.gear/defs.ludic @@ -0,0 +1,55 @@ +# defs.ludic - a kind of gear, a value its tiers give, a charge that burns down, and the facts +# `names`, `cost` and `rep` are per tier to buy (index 0 is the first); `item` is the pack item +# that is the kind (-1 none): bought, it is put in the pack; carried, it counts as the first tier. +# `mult` is per level from 0 (unowned), for the one number a kind scales. +export property GearKind { + key: string = "" + vendor: int = 0 + icon: string = "" + tiers: int = 0 + blurb: string = "" + howto: string = "" + names: []string = null + cost: []int = null + rep: []int = null + item: int = -1 + start: int = 0 # the level a new trip begins at + mult: []float = null +} + +# saved by key, so order is free +export open registry GearKinds of GearKind as GEAR + +# a number a kind's tier decides: `at` per level from 0, the last one holding past its end +export property GearValue { + key: string = "" + gear: int = 0 + at: []float = null +} + +export open registry GearValues of GearValue as GV + +# something that burns down while it runs: `full` units, `per_hour` of them an hour, or the value +# `rate` (-1: per_hour) - a flashlight's drain is its tier's. `hand` runs only while held. +export property GearCharge { + key: string = "" + item: int = -1 + full: float = 100.0 + per_hour: float = 0.0 + rate: int = -1 + hand: bool = true +} + +export open registry GearCharges of GearCharge as CHARGE + +export const GEAR_BOUGHT: int = 0 # a tier was bought +export const GEAR_RAN_OUT: int = 1 # a charge ran out while running + +export property GearFact { + what: int = 0 + kind: int = -1 + level: int = 0 + cost: int = 0 + charge: int = -1 + item: int = -1 +} diff --git a/packages/ludic.gear/hand.ludic b/packages/ludic.gear/hand.ludic new file mode 100644 index 00000000..739906c2 --- /dev/null +++ b/packages/ludic.gear/hand.ludic @@ -0,0 +1,72 @@ +# hand.ludic - what is held, and the charges: a held one runs only while it is in the hand, and +# each burns down by the game hours while it runs +export function gear_hand() -> int { return gear__hand } + +# a new thing in the hand (-1 empty): whatever the old one was running goes out +export function gear_set_hand(it: int) -> void { + gear__ensure() + gear__hand = it + for c in 0 .. CHARGE_COUNT { if GearCharges[c].hand { gear__on[c] = false } } +} + +# the charge an item carries, -1 none +export function gear_charge_of(it: int) -> int { + for c in 0 .. CHARGE_COUNT { if GearCharges[c].item == it { return c } } + return -1 +} + +export function gear_charge(c: int) -> float { + gear__ensure() + return gear__left[c] +} + +export function gear_charge_set(c: int, v: float) -> void { + gear__ensure() + gear__left[c] = Math.clamp(v, 0.0, GearCharges[c].full) +} + +export function gear_charge_fill(c: int) -> void { gear_charge_set(c, GearCharges[c].full) } + +export function gear_running(c: int) -> bool { + gear__ensure() + return gear__on[c] +} + +# on or off; false when it has nothing left, or is held and not in the hand +export function gear_run(c: int, on: bool) -> bool { + gear__ensure() + if not on { + gear__on[c] = false + return true + } + if gear__left[c] < 1.0 { return false } + if GearCharges[c].hand and GearCharges[c].item != gear__hand { return false } + gear__on[c] = true + return true +} + +# the hand's own charge is running (a torch lit, a flashlight on) +export function gear_lit() -> bool { + if gear__hand < 0 { return false } + let c = gear_charge_of(gear__hand) + return c >= 0 and GearCharges[c].hand and gear_running(c) +} + +function gear__rate(c: int) -> float { + let d = GearCharges[c] + if d.rate >= 0 { return gear_value(d.rate) } + return d.per_hour +} + +# the game hours of a tick burn every running charge; one that runs out stops and says so +function gear__burn(hours: float) -> void { + gear__ensure() + for c in 0 .. CHARGE_COUNT { + if not gear__on[c] { continue } + gear__left[c] = gear__left[c] - hours * gear__rate(c) + if gear__left[c] > 0.0 { continue } + gear__left[c] = 0.0 + gear__on[c] = false + gear__fact(GEAR_RAN_OUT, -1, c, 0) + } +} diff --git a/packages/ludic.gear/index.ludic b/packages/ludic.gear/index.ludic new file mode 100644 index 00000000..d835a61f --- /dev/null +++ b/packages/ludic.gear/index.ludic @@ -0,0 +1,12 @@ +# ludic.gear - kinds with tiers, costs and per-tier values; buying through a purse; the hand and +# its charges. The kinds, their words and their items are the game's. +module ludic_gear uses ludic_base +numbers float +import "ludic.base" +import "defs.ludic" +import "ports.ludic" +import "state.ludic" +import "tiers.ludic" +import "buy.ludic" +import "hand.ludic" +import "system.ludic" diff --git a/packages/ludic.gear/package.ludic b/packages/ludic.gear/package.ludic new file mode 100644 index 00000000..ed651065 --- /dev/null +++ b/packages/ludic.gear/package.ludic @@ -0,0 +1,6 @@ +# ludic.gear - tiers of kit bought one at a time with money and standing, what each tier is worth, +# what is in the hand, and the charges that burn down while in use (a torch, a battery). Uses +# ludic.base and nothing else. See README.md. +package "ludic.gear" +version "0.1.0" +kind source diff --git a/packages/ludic.gear/ports.ludic b/packages/ludic.gear/ports.ludic new file mode 100644 index 00000000..a4c6df7b --- /dev/null +++ b/packages/ludic.gear/ports.ludic @@ -0,0 +1,20 @@ +# ports.ludic - what gear asks the game: money and standing to buy with, the pack, and whether a +# kind is on sale at all. Unbound, the pack is empty and takes nothing, and every kind is on sale. +export port GearPurse { + money: fn() -> int + spend: fn(int) -> bool + rep: fn() -> int +} + +export port GearPack { + count: fn(int) -> int = fn gear__none + add: fn(int, int) -> int = fn gear__took_none +} + +export port GearWorld { + hidden: fn(int) -> bool = fn gear__never +} + +function gear__none(it: int) -> int { return 0 } +function gear__took_none(it: int, n: int) -> int { return 0 } +function gear__never(k: int) -> bool { return false } diff --git a/packages/ludic.gear/state.ludic b/packages/ludic.gear/state.ludic new file mode 100644 index 00000000..e90e5cee --- /dev/null +++ b/packages/ludic.gear/state.ludic @@ -0,0 +1,37 @@ +# state.ludic - the level of each kind, the hand, each charge's units and whether it runs, the facts +var gear__lv: []int = null +var gear__hand: int = -1 +var gear__left: []float = null +var gear__on: []bool = null +var gear__q: Queue = null + +function gear__ensure() -> void { + if gear__lv != null { return } + gear__lv = new []int + for k in 0 .. GEAR_COUNT { push(gear__lv, GearKinds[k].start) } + gear__left = new []float + gear__on = new []bool + for c in 0 .. CHARGE_COUNT { + push(gear__left, GearCharges[c].full) + push(gear__on, false) + } +} + +export function gear_facts() -> Queue { + if gear__q == null { gear__q = queue_new("gear.facts") } + return gear__q +} + +function gear__fact(what: int, k: int, c: int, cost: int) -> void { + let f = new GearFact + f.what = what + f.cost = cost + f.kind = k + f.charge = c + if k >= 0 { + f.level = gear__lv[k] + f.item = GearKinds[k].item + } + if c >= 0 { f.item = GearCharges[c].item } + q_push(gear_facts(), f) +} diff --git a/packages/ludic.gear/system.ludic b/packages/ludic.gear/system.ludic new file mode 100644 index 00000000..0ffa2c4d --- /dev/null +++ b/packages/ludic.gear/system.ludic @@ -0,0 +1,49 @@ +# system.ludic - gear as a system: the burn each tick, and the levels and charges saved by key (the +# hand is not saved: a trip opens with empty hands) +export function gear_reset() -> void { + gear__ensure() + for k in 0 .. GEAR_COUNT { gear__lv[k] = GearKinds[k].start } + for c in 0 .. CHARGE_COUNT { + gear__left[c] = GearCharges[c].full + gear__on[c] = false + } + gear__hand = -1 + q_clear(gear_facts()) +} + +export function gear_save() -> Val { + gear__ensure() + let v = Value.object() + let lv = Value.object() + for k in 0 .. GEAR_COUNT { sv_put_int(lv, GearKinds[k].key, gear__lv[k]) } + Value.put(v, "levels", lv) + let ch = Value.object() + for c in 0 .. CHARGE_COUNT { sv_put_float(ch, GearCharges[c].key, gear__left[c]) } + Value.put(v, "charges", ch) + return v +} + +# a kind the file does not name starts where a new trip does; then what the pack carries counts +export function gear_load(v: Val, version: int) -> void { + gear_reset() + if Value.has(v, "levels") != 0 { + let lv = Value.get(v, "levels") + for k in 0 .. GEAR_COUNT { gear__lv[k] = sv_int(lv, GearKinds[k].key, GearKinds[k].start) } + } + if Value.has(v, "charges") != 0 { + let ch = Value.get(v, "charges") + for c in 0 .. CHARGE_COUNT { gear_charge_set(c, sv_float(ch, GearCharges[c].key, GearCharges[c].full)) } + } + gear_sync() +} + +function gear__tick(t: Tick) -> void { gear__burn(t.hours) } + +export function gear_system() -> System { + let s = system_new("gear", PH_SIMULATE) + s.reset = fn gear_reset + s.tick = fn gear__tick + s.save = fn gear_save + s.load = fn gear_load + return s +} diff --git a/packages/ludic.gear/tests/gear_test.ludic b/packages/ludic.gear/tests/gear_test.ludic new file mode 100644 index 00000000..a8c07c45 --- /dev/null +++ b/packages/ludic.gear/tests/gear_test.ludic @@ -0,0 +1,163 @@ +# gear_test.ludic - tiers bought with a fake purse once the standing is there, the item that follows, +# values by tier, the stack's room, a hand whose charges burn down and run out, the save by key +import "ludic.gear" +import "ludic.base" +program GearTest { + numbers float + const IT_ROD: int = 3 + const IT_TORCH: int = 5 + const IT_LAMP: int = 6 + + def GearKinds rod { tiers: 3, names: ["Rod", "Reel", "Guide's rod"], cost: [30, 100, 300], rep: [0, 10, 40], item: IT_ROD } + def GearKinds camera { tiers: 2, names: ["Camera", "Zoom"], cost: [0, 150], rep: [0, 20], start: 1 } + def GearKinds pack { tiers: 3, names: ["Pack", "Frame", "Hauler"], cost: [20, 60, 120], rep: [0, 0, 0], mult: [1.0, 1.3, 1.7, 2.2] } + def GearValues bite { gear: GEAR_ROD, at: [1.0, 1.0, 0.75, 0.55] } + def GearValues drain { gear: GEAR_CAMERA, at: [14.0, 14.0, 8.0] } + def GearCharges torch { item: IT_TORCH, full: 60.0, per_hour: 60.0 } + def GearCharges lamp { item: IT_LAMP, full: 100.0, rate: GV_DRAIN } + def GearCharges radio { item: 9, full: 100.0, per_hour: 5.0, hand: false } + + var money: int = 0 + var rep: int = 0 + var pack: []int = null + var hide_pack: bool = false + + function purse_money() -> int { return money } + function purse_spend(n: int) -> bool { + if n > money { return false } + money -= n + return true + } + function purse_rep() -> int { return rep } + function pack_count(it: int) -> int { return pack[it] } + function pack_add(it: int, n: int) -> int { + pack[it] = pack[it] + n + return n + } + function hidden(k: int) -> bool { return hide_pack and k == GEAR_PACK } + + bind GearPurse { money: fn purse_money, spend: fn purse_spend, rep: fn purse_rep } + bind GearPack { count: fn pack_count, add: fn pack_add } + bind GearWorld { hidden: fn hidden } + + function fresh() -> void { + money = 0 + rep = 0 + hide_pack = false + pack = new []int + for i in 0 .. 12 { push(pack, 0) } + gear_reset() + } + + function ran_out() -> int { + let fs = q_drain(gear_facts()) + var n = -1 + for i in 0 .. len(fs) { if fs[i].what == GEAR_RAN_OUT { n = fs[i].charge } } + return n + } + + test "a new trip starts where the kinds say" { + fresh() + expect_eq(gear_level(GEAR_ROD), 0) + expect_eq(gear_level(GEAR_CAMERA), 1) + expect(gear_next_name(GEAR_CAMERA) == "Zoom") + expect_eq(gear_next_cost(GEAR_ROD), 30) + } + + test "bought with money once the standing is there, and the item follows" { + fresh() + expect(not gear_buy(GEAR_ROD)) + money = 200 + expect(gear_buy(GEAR_ROD)) + expect_eq(money, 170) + expect_eq(pack[IT_ROD], 1) + expect(not gear_can_buy(GEAR_ROD)) + rep = 10 + expect(gear_buy(GEAR_ROD)) + expect_eq(pack[IT_ROD], 1) + let fs = q_drain(gear_facts()) + expect_eq(len(fs), 2) + expect_eq(fs[1].what, GEAR_BOUGHT) + expect_eq(fs[1].level, 2) + expect_eq(fs[1].cost, 100) + } + + test "the best tier is the end, and a hidden kind is not for sale" { + fresh() + money = 1000 + rep = 50 + for i in 0 .. 5 { gear_buy(GEAR_PACK) } + expect(gear_best(GEAR_PACK)) + expect_eq(gear_level(GEAR_PACK), 3) + fresh() + money = 100 + hide_pack = true + expect(not gear_buy(GEAR_PACK)) + expect_eq(len(gear_kinds_at(-1)), 2) + } + + test "a tier decides a value and a mult, and the stack's room follows" { + fresh() + expect_eq(gear_value(GV_BITE), 1.0) + gear_set_level(GEAR_ROD, 3) + expect_eq(gear_value(GV_BITE), 0.55) + expect_eq(gear_mult(GEAR_PACK), 1.0) + gear_set_level(GEAR_PACK, 2) + expect_eq(gear_mult(GEAR_PACK), 1.7) + expect_eq(gear_stack_limit(10, gear_mult(GEAR_PACK)), 17) + expect_eq(gear_stack_limit(1, 3.0), 1) + expect_eq(gear_stack_limit(4, 0.1), 1) + expect_eq(gear_mult(GEAR_ROD), 1.0) + } + + test "a carried item counts as the first tier" { + fresh() + pack[IT_ROD] = 1 + gear_sync() + expect_eq(gear_level(GEAR_ROD), 1) + } + + test "a held charge runs only in the hand, burns by the hour and runs out" { + fresh() + let t = CHARGE_TORCH + expect(not gear_run(t, true)) + gear_set_hand(IT_TORCH) + expect(gear_run(t, true)) + expect(gear_lit()) + gear_system().tick(tick_new(0.1, 1, 0.5)) + expect_eq(gear_charge(t), 30.0) + gear_system().tick(tick_new(0.1, 2, 0.6)) + expect_eq(gear_charge(t), 0.0) + expect(not gear_lit()) + expect_eq(ran_out(), t) + expect(not gear_run(t, true)) + } + + test "a charge's rate can be its tier's, and a new hand puts the light out" { + fresh() + gear_set_hand(IT_LAMP) + gear_run(CHARGE_LAMP, true) + gear_system().tick(tick_new(0.1, 1, 1.0)) + expect_eq(gear_charge(CHARGE_LAMP), 86.0) + gear_set_level(GEAR_CAMERA, 2) + gear_system().tick(tick_new(0.1, 2, 1.0)) + expect_eq(gear_charge(CHARGE_LAMP), 78.0) + gear_run(CHARGE_RADIO, true) + gear_set_hand(-1) + expect(not gear_running(CHARGE_LAMP)) + expect(gear_running(CHARGE_RADIO)) + } + + test "levels and charges are saved by key" { + fresh() + gear_set_level(GEAR_ROD, 2) + gear_charge_set(CHARGE_TORCH, 12.5) + let v = gear_save() + fresh() + gear_load(v, 1) + expect_eq(gear_level(GEAR_ROD), 2) + expect_eq(gear_level(GEAR_CAMERA), 1) + expect_eq(gear_charge(CHARGE_TORCH), 12.5) + expect_eq(gear_charge(CHARGE_LAMP), 100.0) + } +} diff --git a/packages/ludic.gear/tiers.ludic b/packages/ludic.gear/tiers.ludic new file mode 100644 index 00000000..69a10adc --- /dev/null +++ b/packages/ludic.gear/tiers.ludic @@ -0,0 +1,72 @@ +# tiers.ludic - what is owned and what a tier is worth +export function gear_ok(k: int) -> bool { return k >= 0 and k < GEAR_COUNT } + +export function gear_level(k: int) -> int { + gear__ensure() + return gear__lv[k] +} + +# outright: staging, a test, a camera handed over with the trip +export function gear_set_level(k: int, lv: int) -> void { + gear__ensure() + gear__lv[k] = lv +} + +export function gear_owned(k: int) -> bool { return gear_level(k) > 0 } + +export function gear_best(k: int) -> bool { return gear_level(k) >= GearKinds[k].tiers } + +# a tier's name, "" for none +export function gear_level_name(k: int, lv: int) -> string { + if lv <= 0 or lv > GearKinds[k].tiers { return "" } + return GearKinds[k].names[lv - 1] +} + +# the tier on offer next: its name, cost and the standing it wants +export function gear_next_name(k: int) -> string { return gear_level_name(k, gear_level(k) + 1) } + +export function gear_next_cost(k: int) -> int { + if gear_best(k) { return 0 } + return GearKinds[k].cost[gear_level(k)] +} + +export function gear_next_rep(k: int) -> int { + if gear_best(k) { return 0 } + return GearKinds[k].rep[gear_level(k)] +} + +function gear__pick(xs: []float, lv: int, fallback: float) -> float { + if xs == null or len(xs) == 0 { return fallback } + if lv >= len(xs) { return xs[len(xs) - 1] } + if lv < 0 { return xs[0] } + return xs[lv] +} + +# the kind's own number at its level (1.0 when it has none) +export function gear_mult(k: int) -> float { return gear__pick(GearKinds[k].mult, gear_level(k), 1.0) } + +# a value its kind's tier decides +export function gear_value(v: int) -> float { + let d = GearValues[v] + return gear__pick(d.at, gear_level(d.gear), 0.0) +} + +# the room for a thing whose base stack is `base`, scaled: a single thing stays single, and a +# stack never falls below one +export function gear_stack_limit(base: int, mult: float) -> int { + if base <= 1 { return base } + let s = int(float(base) * mult) + if s < 1 { return 1 } + return s +} + +# the kinds a vendor sells (-1 every vendor), leaving out what the game hides +export function gear_kinds_at(vendor: int) -> []int { + let out = new []int + for k in 0 .. GEAR_COUNT { + if vendor >= 0 and GearKinds[k].vendor != vendor { continue } + if GearWorld.hidden(k) { continue } + push(out, k) + } + return out +}