Understand and work with the currency, wealth, transaction, shop, bank, coin, and loot-value systems in Oxidus. Covers the currency daemon, wealth tracking, transaction math, inventory-based shops (EXT_SHOP), menu-based shops (EXT_SHOP_MENU), the bank daemon, coin objects, and item values.
You are helping work with the Oxidus currency, wealth, and shop systems. Follow the lpc-coding-style skill for all LPC formatting.
CURRENCY_D (adm/daemons/currency.c) — currency registry, conversion
|
v
wealth.c (std/living/wealth.c) — per-living coin storage
|
v
EXT_CURRENCY (std/ext/currency.c) — transaction handling
├── EXT_SHOP (std/ext/shop.c) — inventory-based shop (has storage object)
└── EXT_SHOP_MENU (std/ext/shop_menu.c) — menu-based shop (clones on demand)
BANK_D (adm/daemons/bank.c) — SQLite-backed bank accounts
└── EXT_BANK (std/ext/bank.c) — room commands for banking
LIB_COIN (lib/coin.c) — physical coin objects
STD_VALUE (std/object/value.c) — item monetary value
LOOT_D (adm/daemons/loot.c) — loot/coin drops on death
From adm/etc/default.lpml:
CURRENCY: [
["copper", 1 ],
["silver", 10 ],
["gold", 100 ],
["platinum", 1000],
]
Each entry is [name, value_in_base_units]. Copper = 1 is the base denomination. All internal values are in base (copper) units.
Related config:
| Key | Default | Purpose |
|---|---|---|
USE_MASS | true | Coins have physical weight |
COIN_VALUE_PER_LEVEL | 15 | NPC loot coin value scaling |
COIN_VARIANCE | 0.25 | 25% variance on loot values |
STORAGE_DATA_DIR | "/data/storage/" | Persistent storage file path |
adm/daemons/currency.cSingleton daemon. Macro: CURRENCY_D.
| Function | Signature | Description |
|---|---|---|
valid_currency_type | int (string currency) | Returns 1 if valid denomination |
convert_currency | int (int amount, string from, string to) | Converts between denominations. Uses amount * from_rate / to_rate, rounded. Returns -1 on invalid. |
fconvert_currency | float (int amount, string from, string to) | Same as above but returns raw float |
lowest_currency | string () | Returns "copper" |
highest_currency | string () | Returns "platinum" |
currency_list | string* () | Returns names ordered lowest to highest |
currency_value | int (string currency) | Returns base-unit value of a denomination |
get_currency_map | mapping () | Returns ([ name: value, ... ]) copy |
std/living/wealth.cInherited by STD_BODY. Tracks coins as a mapping of { currency_name: count }.
private nomask mapping _wealth = ([]); // e.g., ([ "copper": 50, "gold": 3 ])
| Function | Signature | Description |
|---|---|---|
query_total_coins | int () | Sum of all coin counts (ignores denomination) |
query_total_wealth | int () | Total value in base units. 1 gold + 5 copper = 105 |
query_all_wealth | mapping () | Safe copy of _wealth |
query_wealth | int (string currency) | Count for a specific denomination |
adjust_wealth | mixed (string currency, int amount) | Returns int on success, string on error. Validates currency, checks sufficient funds for negatives, checks mass capacity if USE_MASS. Sends GMCP. Calls rehash_capacity() |
set_wealth | mapping (mapping w) | Replaces entire wealth. Wipes first, then adjusts each |
init_wealth | void () | Initializes to ([]) if null |
wipe_wealth | void () | Clears to ([]), sends GMCP, rehashes capacity |
Important: adjust_wealth returns mixed — it can return an error string if the currency is invalid, funds are insufficient, or the player can't carry the weight. Always check the return type.
std/ext/currency.cMacro: EXT_CURRENCY. Inherited by both shop modules.
handle_transaction(object tp, int cost) : mixedEntry point for all purchases. cost is in base (copper) units.
Returns either:
string — error message({ paid_array, change_array }) — successWhere each array contains ({ currency_name, amount }) pairs.
complex_transaction)total_wealth in base units. Fail if insufficient.min(available, ceil(remaining / rate)) coins.adjust_wealth(currency, -amount) for each payment, adjust_wealth(currency, amount) for each change coin.reverse_transaction(object tp, mixed result) : mixedCritical for error recovery. Reverses a completed transaction when a subsequent operation fails (e.g., item won't fit in inventory).
// In a buy command:
mixed result = handle_transaction(tp, cost);
if(stringp(result)) return result; // payment failed
if(stringp(ob->move(tp))) {
reverse_transaction(tp, result); // MUST reverse or coins are lost
return "You can't carry that.";
}
least_coins(int total_amount) : mappingBreaks a base-unit amount into the minimum number of coins per denomination. Does not deduct anything — pure calculation.
least_coins(125) // → ([ "gold": 1, "silver": 2, "copper": 5 ])
Used by EXT_SHOP's sell command to compute payout.
| Function | Description |
|---|---|
format_return_currency_string(mixed *arr) | Converts ({ ({"gold", 2}), ({"silver", 3}) }) to "2 gold, 3 silver" |
can_afford(object ob, int cost, string currency) | Single-denomination check |
format_currency(int amount, string currency) | Returns "<amount> <currency>" |
std/ext/shop.cMacro: EXT_SHOP. Inherits EXT_CURRENCY and CLASS_STORAGE.
Items are stored in a persistent STD_STORAGE_OBJECT. Players buy items out of it and sell items into it.
inherit STD_ROOM;
inherit EXT_SHOP;
void setup() {
// ... room descriptions, exits, etc. ...
init_shop();
add_shop_inventory(
"/obj/weapon/piercing/rusty_sword",
"/obj/armour/torso/leather_jerkin",
);
}
| Variable | Type | Default | Description |
|---|---|---|---|
shop_open | int | 1 | Shop open/closed flag |
allow_npcs | int | 0 | Whether NPCs can buy/sell |
sell_factor | float | 0.5 | Multiplier on item value when selling |
store | object | — | The storage object holding inventory |
shop_inventory | mixed* | ({}) | Registered item files for restocking |
buy <item>: Finds item in storage via present(str, store). Calls handle_transaction(tp, cost). Moves item to player. On move failure, calls reverse_transaction. Sends action messages.
sell <item> / sell all / sell all <item>: Finds items on player. For each: checks query_cost(tp, ob, "sell") (returns null to refuse), skips equipped items, moves to store, pays player via least_coins(cost) → adjust_wealth() per denomination.
list: Shows all items in storage with prices.
query_cost()| Transaction | Price |
|---|---|
"buy" | ob->query_value() |
"sell" | to_int(to_float(ob->query_value()) * sell_factor) |
"list" | ob->query_value() |
Created via create_storage() using class StorageOptions:
class StorageOptions {
string storage_type; // "public" or "private"
mixed storage_id; // ID string or function
string storage_org; // namespace
string storage_directory; // filesystem path
int clean_on_empty; // remove when empty
int restore_on_load; // restore from saved data
}
The storage object has ignore_capacity(1) and ignore_mass(1) — no limits on stored items.
Known issue: create_storage() currently has a hardcoded storage_org. Each shop must override this or they share storage.
std/ext/shop_menu.cMacro: EXT_SHOP_MENU. Inherits CLASS_MENU and EXT_CURRENCY.
Items are cloned fresh on each purchase. No storage object. No sell command. Ideal for food, drink, and consumable vendors.
inherit STD_ROOM;
inherit EXT_SHOP_MENU;
void setup() {
// ... room descriptions, exits, etc. ...
init_shop();
add_menu_item("food", "/obj/food/bread", 5);
add_menu_item("drink", "/obj/drink/ale", 8);
}
class Menuclass Menu {
string type; // category (e.g., "food", "drink")
string file; // blueprint file path
string short; // display name
string *id; // ID array for matching
string *adj; // adjective array
string description; // long description
int cost; // price in base units
}
| Function | Description |
|---|---|
add_menu_item(string type, string file, int cost) | Loads blueprint, populates Menu class. Cost defaults to ob->query_value() if 0/null |
remove_menu_item(string file) | Remove by file path |
wipe_menu() | Clear all items |
buy <item>: Searches food_menu by ID. Calls handle_transaction. Clones new(item.file). Moves to player. Reverses on failure.
list / menu [type]: Shows all items or filtered by type with prices.
view <item>: Shows item short + long description.
| Feature | EXT_SHOP | EXT_SHOP_MENU |
|---|---|---|
| Items persist between purchases | Yes (storage object) | No (cloned fresh) |
| Players can sell items back | Yes | No |
| Items are unique/individual | Yes | No (all identical clones) |
| Best for | Weapons, armor, gear | Food, drinks, consumables |
| Restocking | Via reset_shop() | Infinite (clone on demand) |
adm/daemons/bank.cSQLite-backed via DB_D. Stores a single balance in base (copper) units.
| Function | Signature | Description |
|---|---|---|
new_account | mixed (string name) | Creates account with balance 0 |
query_balance | mixed (string name) | Returns int balance, null if not found, or error string |
add_balance | mixed (string name, int amount) | Adjusts balance (negative for withdrawals). Fails if result < 0 |
query_activity | mixed (string name, int limit) | Returns activity log array. Default limit 10 |
std/ext/bank.cMacro: EXT_BANK. Provides room commands.
inherit STD_ROOM;
inherit EXT_BANK;
void setup() {
// ... room setup ...
init_bank();
}
Commands: register, deposit <num> <type>, withdraw <num> <type>, balance.
Key design: Deposits convert denomination to copper for storage. Withdrawals convert copper back to the requested denomination. You can deposit 1 gold and withdraw 100 copper.
lib/coin.cPhysical coin items that exist in rooms and containers.
dest->adjust_wealth(coin_type, coin_num). Coins dissolve into the wealth mapping.adjust_coin_num(). Different types coexist as separate objects.set_mass(num)).new(LIB_COIN, "gold", 5) // creates a stack of 5 gold coins
| Function | Returns | Description |
|---|---|---|
query_coin_type() | string | Denomination name |
query_coin_num() | int | Stack size |
query_value() | ({ coin_num, coin_type }) | Returns array, NOT int |
query_base_value() | int | Total copper-equivalent value |
is_coin() | 1 | Type identifier |
adjust_coin_num(int) | int | Adjusts stack. Removes self if count reaches 0 |
Critical: query_value() on a coin returns ({ num, type }) — an array, not an integer. This intentionally differs from STD_VALUE::query_value() which returns int. Code handling mixed item types must account for this.
std/object/value.cAll items inherit this via STD_ITEM.
set_value(50); // 50 copper
query_value(); // 50
adjust_value(10); // now 60 (but not auto-persisted!)
Note: set_value() calls save_var("_value") for persistence, but adjust_value() does not. Changes via adjust_value() are lost on reload unless save_var is called separately.
When NPCs drop loot, LOOT_D can auto-value items:
determine_value_by_level(level):
value = level * COIN_VALUE_PER_LEVEL (15)
variance = COIN_VARIANCE (0.25) * value
subtract = to_int(variance) / 2
value -= subtract
value += random(to_int(variance) + 1)
Level 10 NPC: base 150, range ~131-169 copper.
Triggered when a loot item has query_loot_property("autovalue") == true.
Wealth changes send GMCP from adjust_wealth() and wipe_wealth():
Package: "Char.Status"
Label: "wealth"
Payload: ([ currency_name: sprintf("%d", amount) ])
No GMCP is sent directly by shop modules. Item movement triggers GMCP_PKG_CHAR_ITEMS_LIST via STD_ITEM::move().
On die() in body.c:
query_all_wealth() is iterated.new(LIB_COIN, type, amount).set_value(100) means 100 copper = 1 gold. Shops, transactions, and bank all work in copper.adjust_wealth() can silently fail. It returns a string on error (invalid currency, insufficient funds, over encumbrance). Always check the return type.reverse_transaction() is mandatory on buy failure. If handle_transaction succeeds but the item can't be moved to the player, you must call reverse_transaction or coins are duplicated/lost.EXT_SHOP for persistent inventory (gear shops). EXT_SHOP_MENU for clone-on-demand (food/drink vendors). Using EXT_SHOP for consumables causes persistent item accumulation.query_value() on coins returns an array, not int. Code that handles mixed items must check for this.adjust_value() does not persist. Only set_value() calls save_var().