Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d24f7f03a3 | |||
| 28a02d0ced | |||
| d5fd2c97e9 | |||
| ae061a5035 | |||
| 6264785f2c | |||
| 998ec21bb5 | |||
| aa881bfc65 | |||
| 3cec2cb69b | |||
| c699dcc77b | |||
| f39205404a | |||
| b3d46931b2 |
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"default_packs": [
|
||||||
|
"file/Computer Craft Recreated v1.2.zip"
|
||||||
|
],
|
||||||
|
"default_overrides": [
|
||||||
|
{
|
||||||
|
"id": "file/Computer Craft Recreated v1.2.zip",
|
||||||
|
"default_position": "TOP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Brass & Sigil KubeJS scripts
|
||||||
|
|
||||||
|
All custom server behaviour lives here. Files in this tree ship via the
|
||||||
|
modpack manifest (see `scripts/Build-Pack.ps1` — `kubejs/` is in
|
||||||
|
`managedRoots`), get synced to both server and client installs, and are
|
||||||
|
loaded by KubeJS on startup.
|
||||||
|
|
||||||
|
## File layout
|
||||||
|
|
||||||
|
```
|
||||||
|
kubejs/
|
||||||
|
├── README.md this file
|
||||||
|
│
|
||||||
|
├── server_scripts/ Loaded by the SERVER's KubeJS on world load.
|
||||||
|
│ ├── recipes_<x>.js Crafting-recipe overhauls per mod.
|
||||||
|
│ ├── waystones_policy.js Waystones placement gating (1/player + bans).
|
||||||
|
│ ├── welcome.js First-join grant: waystone + manual book.
|
||||||
|
│ ├── cc_recipes.js Full ComputerCraft recipe overhaul.
|
||||||
|
│ └── bns_admin.js /bns admin <subcommand> for OP diagnostics.
|
||||||
|
│
|
||||||
|
└── client_scripts/ Loaded by the CLIENT's KubeJS on game load.
|
||||||
|
└── jei_hides.js Hide specific items from the JEI ingredient list.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Logging prefix**: every console line uses `[bns/<module>]` so server
|
||||||
|
logs can be filtered with `journalctl -u brass-sigil-server.service |
|
||||||
|
grep '\[bns/'`. Keeps our output separate from mod output.
|
||||||
|
- **One concern per file**. Recipe overhauls live in `recipes_<modname>`.
|
||||||
|
Block/item policy goes in a `<thing>_policy.js`. Don't mix recipe
|
||||||
|
removal with event listeners in the same file.
|
||||||
|
- **Persistent player data** keys are prefixed `bns` to avoid clashing
|
||||||
|
with other mods' use of `player.persistentData`. Currently used keys:
|
||||||
|
- `bnsWaystoneCount` (int) — see `waystones_policy.js`
|
||||||
|
- `bnsWelcomeGranted` (bool) — see `welcome.js`
|
||||||
|
- **Performance**: hot-path event handlers (BlockEvents.placed/broken,
|
||||||
|
PlayerEvents.loggedIn) must stay O(1). Use `Set` for ID lookups, never
|
||||||
|
Array.includes inside an event handler. Avoid Modrinth/network calls
|
||||||
|
in event handlers — never. No periodic-tick polling unless absolutely
|
||||||
|
necessary.
|
||||||
|
- **Server commands** are registered under the `/bns` namespace
|
||||||
|
(`/bns admin ...`, future: `/bns plot ...`). See `bns_admin.js` for
|
||||||
|
the registration pattern.
|
||||||
|
|
||||||
|
## Diagnostics
|
||||||
|
|
||||||
|
When something looks wrong in-game, first port of call:
|
||||||
|
|
||||||
|
1. `/bns admin info` — prints active flags + counts for the running player
|
||||||
|
2. `/bns admin waystone-count <player>` — read stored count
|
||||||
|
3. `/bns admin reset-welcome <player>` — re-trigger the first-join grant
|
||||||
|
4. Server log: `journalctl -u brass-sigil-server.service -f | grep '\[bns/'`
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- `pack/overrides/defaultconfigs/` — first-run mod configs (auto-copied
|
||||||
|
to `config/` by NeoForge on first launch only)
|
||||||
|
- `scripts/Check-Deps.ps1` — pre-flight dep check, runs before any
|
||||||
|
Deploy-Brass `-Stage Pack` deploy
|
||||||
|
- The memory file `project-mc-player-economy.md` for the broader plan
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// Brass & Sigil — JEI hides
|
|
||||||
//
|
|
||||||
// Hides all Waystones items + all Numismatics coin items from the JEI
|
|
||||||
// item list. Combined with the server-side recipe removal in
|
|
||||||
// economy_policy.js, players don't see crafting recipes OR the item
|
|
||||||
// icons in the JEI ingredient list -- they only ever encounter these
|
|
||||||
// items through the welcome grant or the bank exchange.
|
|
||||||
|
|
||||||
const COLORS = [
|
|
||||||
'white', 'orange', 'magenta', 'light_blue', 'yellow', 'lime',
|
|
||||||
'pink', 'gray', 'light_gray', 'cyan', 'purple', 'blue',
|
|
||||||
'brown', 'green', 'red', 'black',
|
|
||||||
];
|
|
||||||
|
|
||||||
const WAYSTONE_ITEMS = [
|
|
||||||
// regular waystone variants
|
|
||||||
'waystones:waystone',
|
|
||||||
'waystones:mossy_waystone',
|
|
||||||
'waystones:sandy_waystone',
|
|
||||||
'waystones:blackstone_waystone',
|
|
||||||
'waystones:deepslate_waystone',
|
|
||||||
'waystones:end_stone_waystone',
|
|
||||||
// handheld + plate
|
|
||||||
'waystones:warp_stone',
|
|
||||||
'waystones:warp_scroll',
|
|
||||||
'waystones:bound_scroll',
|
|
||||||
'waystones:return_scroll',
|
|
||||||
'waystones:warp_plate',
|
|
||||||
// sharestones (16 colors) + shards
|
|
||||||
'waystones:sharestone', // base id if registered
|
|
||||||
];
|
|
||||||
COLORS.forEach(c => {
|
|
||||||
WAYSTONE_ITEMS.push(`waystones:${c}_sharestone`);
|
|
||||||
WAYSTONE_ITEMS.push(`waystones:${c}_portstone`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const COIN_ITEMS = [
|
|
||||||
'numismatics:spur',
|
|
||||||
'numismatics:bevel',
|
|
||||||
'numismatics:cog',
|
|
||||||
'numismatics:crown',
|
|
||||||
'numismatics:sun',
|
|
||||||
];
|
|
||||||
|
|
||||||
JEIEvents.hideItems(event => {
|
|
||||||
[...WAYSTONE_ITEMS, ...COIN_ITEMS].forEach(id => {
|
|
||||||
try { event.hide(id); } catch (e) {
|
|
||||||
// Some IDs may not exist (e.g. base 'waystones:sharestone' isn't
|
|
||||||
// registered if only colored variants are). Swallow per-item
|
|
||||||
// failures so one bad id doesn't break the whole pass.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Brass & Sigil — admin commands (/bns admin ...)
|
||||||
|
//
|
||||||
|
// Diagnostic + maintenance commands for OPs. Players without OP level
|
||||||
|
// (>= 2) get a permission-denied response from the command framework.
|
||||||
|
//
|
||||||
|
// Subcommand summary:
|
||||||
|
// /bns admin info state of the calling player
|
||||||
|
// /bns admin waystone-count <player> read stored count
|
||||||
|
// /bns admin waystone-set <player> <n> override stored count
|
||||||
|
// /bns admin reset-welcome <player> clear bnsWelcomeGranted flag
|
||||||
|
// so the welcome grant fires again
|
||||||
|
//
|
||||||
|
// Adding new subcommands: follow the same `Commands.literal(...)
|
||||||
|
// .then(...)` pattern below. Keep handlers cheap -- these are only run
|
||||||
|
// on operator action so performance isn't critical, but make them
|
||||||
|
// readable.
|
||||||
|
|
||||||
|
ServerEvents.commandRegistry(event => {
|
||||||
|
const { commands: Commands, arguments: Arguments } = event;
|
||||||
|
|
||||||
|
const bnsAdmin = Commands.literal('bns')
|
||||||
|
.then(Commands.literal('admin')
|
||||||
|
.requires(src => src.hasPermission(2))
|
||||||
|
|
||||||
|
// /bns admin info
|
||||||
|
.then(Commands.literal('info').executes(ctx => {
|
||||||
|
const player = ctx.source.player;
|
||||||
|
if (!player) {
|
||||||
|
ctx.source.sendSystemMessage(Text.red('Run from a player context.'));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const data = player.persistentData;
|
||||||
|
const lines = [
|
||||||
|
`Player: ${player.username} (${player.uuid})`,
|
||||||
|
`bnsWaystoneCount = ${data.getInt('bnsWaystoneCount')}`,
|
||||||
|
`bnsWelcomeGranted = ${data.getBoolean('bnsWelcomeGranted')}`,
|
||||||
|
];
|
||||||
|
lines.forEach(line => ctx.source.sendSystemMessage(Text.aqua(line)));
|
||||||
|
console.info(`[bns/admin] info: ${player.username} -> ${JSON.stringify({
|
||||||
|
waystones: data.getInt('bnsWaystoneCount'),
|
||||||
|
welcomed: data.getBoolean('bnsWelcomeGranted'),
|
||||||
|
})}`);
|
||||||
|
return 1;
|
||||||
|
}))
|
||||||
|
|
||||||
|
// /bns admin waystone-count <player>
|
||||||
|
.then(Commands.literal('waystone-count')
|
||||||
|
.then(Commands.argument('target', Arguments.PLAYER.create(event))
|
||||||
|
.executes(ctx => {
|
||||||
|
const target = Arguments.PLAYER.getResult(ctx, 'target');
|
||||||
|
const count = target.persistentData.getInt('bnsWaystoneCount');
|
||||||
|
ctx.source.sendSystemMessage(Text.aqua(
|
||||||
|
`${target.username} has bnsWaystoneCount = ${count}`
|
||||||
|
));
|
||||||
|
return 1;
|
||||||
|
})))
|
||||||
|
|
||||||
|
// /bns admin waystone-set <player> <n>
|
||||||
|
// (Argument-bounds aren't exposed via KubeJS's wrapper, so we
|
||||||
|
// validate the range inside the handler instead.)
|
||||||
|
.then(Commands.literal('waystone-set')
|
||||||
|
.then(Commands.argument('target', Arguments.PLAYER.create(event))
|
||||||
|
.then(Commands.argument('count', Arguments.INTEGER.create(event))
|
||||||
|
.executes(ctx => {
|
||||||
|
const target = Arguments.PLAYER.getResult(ctx, 'target');
|
||||||
|
const count = Arguments.INTEGER.getResult(ctx, 'count');
|
||||||
|
if (count < 0 || count > 1000) {
|
||||||
|
ctx.source.sendSystemMessage(Text.red(
|
||||||
|
`count must be 0..1000 (got ${count})`
|
||||||
|
));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
target.persistentData.putInt('bnsWaystoneCount', count);
|
||||||
|
ctx.source.sendSystemMessage(Text.gold(
|
||||||
|
`Set ${target.username}'s bnsWaystoneCount = ${count}`
|
||||||
|
));
|
||||||
|
console.info(`[bns/admin] waystone-set: ${target.username} -> ${count}`);
|
||||||
|
return 1;
|
||||||
|
}))))
|
||||||
|
|
||||||
|
// /bns admin reset-welcome <player>
|
||||||
|
.then(Commands.literal('reset-welcome')
|
||||||
|
.then(Commands.argument('target', Arguments.PLAYER.create(event))
|
||||||
|
.executes(ctx => {
|
||||||
|
const target = Arguments.PLAYER.getResult(ctx, 'target');
|
||||||
|
target.persistentData.putBoolean('bnsWelcomeGranted', false);
|
||||||
|
ctx.source.sendSystemMessage(Text.gold(
|
||||||
|
`Cleared bnsWelcomeGranted for ${target.username}. ` +
|
||||||
|
`They'll get the welcome packet again on next login.`
|
||||||
|
));
|
||||||
|
console.info(`[bns/admin] reset-welcome: ${target.username}`);
|
||||||
|
return 1;
|
||||||
|
})))
|
||||||
|
);
|
||||||
|
|
||||||
|
event.register(bnsAdmin);
|
||||||
|
});
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// Brass & Sigil — economy policy (recipe removal)
|
|
||||||
//
|
|
||||||
// Players can't craft Waystones items (they're distributed by the welcome
|
|
||||||
// grant -- see welcome.js -- and any replacements come from the bank
|
|
||||||
// vendor in the future). Players also can't manufacture currency: coins
|
|
||||||
// only enter circulation through the bank exchange (item-in / coin-out).
|
|
||||||
//
|
|
||||||
// Other Numismatics blocks (piggy banks, bank tellers, vendor blocks) stay
|
|
||||||
// craftable -- those are infrastructure, not currency.
|
|
||||||
|
|
||||||
ServerEvents.recipes(event => {
|
|
||||||
// Block ALL crafting of Waystones items. The welcome-grant script
|
|
||||||
// gives each new player exactly one regular waystone; any further
|
|
||||||
// recovery happens via OP /give or future bank vendor.
|
|
||||||
event.remove({ mod: 'waystones' });
|
|
||||||
|
|
||||||
// Block coin manufacturing only -- 5 denominations.
|
|
||||||
// Real registry IDs: spur, bevel, cog, crown, sun.
|
|
||||||
[
|
|
||||||
'numismatics:spur',
|
|
||||||
'numismatics:bevel',
|
|
||||||
'numismatics:cog',
|
|
||||||
'numismatics:crown',
|
|
||||||
'numismatics:sun',
|
|
||||||
].forEach(coin => event.remove({ output: coin }));
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
// Brass & Sigil — Plot system V1
|
||||||
|
//
|
||||||
|
// SPAWN COMMERCIAL PLOTS (not bases). Players' homes go out in the world
|
||||||
|
// with normal FTB Chunks claims. A "plot" is a small commercial chunk-
|
||||||
|
// allocation at spawn for shops. 1 plot per player; release-and-rebuy to
|
||||||
|
// change size.
|
||||||
|
//
|
||||||
|
// Player commands (/bns plot ...):
|
||||||
|
// list show all plots + availability + price
|
||||||
|
// info <id> detailed info for one plot
|
||||||
|
// buy <id> purchase if eligible (no current plot, has coin)
|
||||||
|
// release release current plot (50% refund)
|
||||||
|
// Admin commands (OP, level 2):
|
||||||
|
// reload re-load plot definitions (currently in-script)
|
||||||
|
//
|
||||||
|
// Storage:
|
||||||
|
// Plot definitions: hardcoded `PLOT_DEFS` below; admin edits and reloads.
|
||||||
|
// Path-to-disk loading deferred to V1.1 (KubeJS file
|
||||||
|
// I/O is sandboxed).
|
||||||
|
// Ownership state: server.persistentData.bnsPlots -- compound NBT
|
||||||
|
// keyed by plot id, value = player UUID string. Single
|
||||||
|
// source of truth.
|
||||||
|
//
|
||||||
|
// Integrations (best-effort; verify in playtest):
|
||||||
|
// - Numismatics coin handling: inventory-based. Counts coins by denomination
|
||||||
|
// value, takes greedily by largest first. Assumed values:
|
||||||
|
// spur=1, bevel=8, cog=64, crown=512, sun=4096
|
||||||
|
// If Numismatics uses different ratios, adjust COIN_VALUES below.
|
||||||
|
// - FTB Chunks claim transfer: runs `/ftbchunks claim <x> <z>` as the player
|
||||||
|
// for each chunk in the plot, prefixed by `execute in <dim> as <player>`.
|
||||||
|
// Release runs `/ftbchunks unclaim`. May need adjusting if FTBC command
|
||||||
|
// syntax differs in this version.
|
||||||
|
//
|
||||||
|
// Performance:
|
||||||
|
// - All event-handler-style work is bounded by player action (command).
|
||||||
|
// No periodic ticks. No event hot-path scans.
|
||||||
|
// - Plot lookup is O(1) via Map.
|
||||||
|
// - Coin inventory scan is O(36) per purchase -- once per player action.
|
||||||
|
|
||||||
|
// ─── Plot definitions ─────────────────────────────────────────────────
|
||||||
|
// V1 hardcoded. Replace with admin-defined plots once the spawn marketplace
|
||||||
|
// is built. Chunks are [chunkX, chunkZ] pairs in the overworld.
|
||||||
|
const PLOT_DEFS = new Map([
|
||||||
|
['p001', {
|
||||||
|
id: 'p001', name: 'Market Stall 1', size: 'small',
|
||||||
|
chunks: [[10, 10]], // 1x1
|
||||||
|
price: 100, // in spurs
|
||||||
|
dimension: 'minecraft:overworld',
|
||||||
|
}],
|
||||||
|
['p002', {
|
||||||
|
id: 'p002', name: 'Corner Lot', size: 'medium',
|
||||||
|
chunks: [[12, 10], [12, 11]], // 1x2
|
||||||
|
price: 250,
|
||||||
|
dimension: 'minecraft:overworld',
|
||||||
|
}],
|
||||||
|
['p003', {
|
||||||
|
id: 'p003', name: 'Anchor Plot', size: 'large',
|
||||||
|
chunks: [[14, 10], [14, 11], [15, 10], [15, 11]], // 2x2
|
||||||
|
price: 600,
|
||||||
|
dimension: 'minecraft:overworld',
|
||||||
|
}],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const REFUND_FRACTION = 0.5; // released plots refund 50% of purchase price
|
||||||
|
|
||||||
|
// ─── Numismatics coin handling ────────────────────────────────────────
|
||||||
|
const COIN_VALUES = {
|
||||||
|
'numismatics:spur': 1,
|
||||||
|
'numismatics:bevel': 8,
|
||||||
|
'numismatics:cog': 64,
|
||||||
|
'numismatics:crown': 512,
|
||||||
|
'numismatics:sun': 4096,
|
||||||
|
};
|
||||||
|
const DENOMS_DESC = ['numismatics:sun', 'numismatics:crown', 'numismatics:cog',
|
||||||
|
'numismatics:bevel', 'numismatics:spur'];
|
||||||
|
|
||||||
|
function countCoins(player) {
|
||||||
|
let total = 0;
|
||||||
|
const inv = player.inventory;
|
||||||
|
for (let i = 0; i < inv.containerSize; i++) {
|
||||||
|
const stack = inv.getItem(i);
|
||||||
|
const v = COIN_VALUES[stack.id];
|
||||||
|
if (v) total += v * stack.count;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeCoins(player, amount) {
|
||||||
|
let remaining = amount;
|
||||||
|
const inv = player.inventory;
|
||||||
|
// Pass 1: take from largest denomination downward (minimises change).
|
||||||
|
for (const id of DENOMS_DESC) {
|
||||||
|
const value = COIN_VALUES[id];
|
||||||
|
if (remaining < value) continue;
|
||||||
|
for (let i = 0; i < inv.containerSize && remaining >= value; i++) {
|
||||||
|
const stack = inv.getItem(i);
|
||||||
|
if (stack.id !== id) continue;
|
||||||
|
const take = Math.min(stack.count, Math.floor(remaining / value));
|
||||||
|
stack.shrink(take);
|
||||||
|
remaining -= take * value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return amount - remaining; // returns how many spurs actually taken
|
||||||
|
}
|
||||||
|
|
||||||
|
function giveCoins(player, amount) {
|
||||||
|
// Give back as largest denomination possible to minimise inventory clutter.
|
||||||
|
let remaining = amount;
|
||||||
|
for (const id of DENOMS_DESC) {
|
||||||
|
const value = COIN_VALUES[id];
|
||||||
|
if (remaining < value) continue;
|
||||||
|
const count = Math.floor(remaining / value);
|
||||||
|
if (count > 0) {
|
||||||
|
player.give(Item.of(id, count));
|
||||||
|
remaining -= count * value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Ownership storage (server.persistentData.bnsPlots compound) ──────
|
||||||
|
function getOwnerMap() {
|
||||||
|
// Lazily ensure the compound exists. The compound is a plot_id -> uuid map.
|
||||||
|
const root = Utils.server.persistentData;
|
||||||
|
if (!root.contains('bnsPlots')) {
|
||||||
|
root.put('bnsPlots', {});
|
||||||
|
}
|
||||||
|
return root.getCompound('bnsPlots');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ownerOf(plotId) {
|
||||||
|
const map = getOwnerMap();
|
||||||
|
return map.contains(plotId) ? map.getString(plotId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function plotOwnedBy(playerUuid) {
|
||||||
|
const map = getOwnerMap();
|
||||||
|
for (const key of map.getAllKeys()) {
|
||||||
|
if (map.getString(key) === playerUuid) return key;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordPurchase(plotId, playerUuid) {
|
||||||
|
getOwnerMap().putString(plotId, playerUuid);
|
||||||
|
console.info(`[bns/plots] purchase recorded: ${plotId} -> ${playerUuid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearOwner(plotId) {
|
||||||
|
getOwnerMap().remove(plotId);
|
||||||
|
console.info(`[bns/plots] ownership cleared: ${plotId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── FTB Chunks integration (best-effort) ────────────────────────────
|
||||||
|
// FTB Chunks doesn't expose a KubeJS API for "claim chunk X for player Y",
|
||||||
|
// but its commands (/ftbchunks claim, /ftbchunks unclaim) work as
|
||||||
|
// console-as-player. Issue them with `execute as <player>` so they claim
|
||||||
|
// to that player's team.
|
||||||
|
function claimChunksForPlayer(plot, player) {
|
||||||
|
for (const [cx, cz] of plot.chunks) {
|
||||||
|
// /ftbchunks claim claims the chunk the executor stands in by default,
|
||||||
|
// but accepts X Z args in most versions. Try with explicit coords first;
|
||||||
|
// if that fails the syntax may need a `position` subcommand variant.
|
||||||
|
const cmd = `execute as ${player.username} in ${plot.dimension} run ftbchunks claim ${cx} ${cz}`;
|
||||||
|
Utils.server.runCommandSilent(cmd);
|
||||||
|
console.info(`[bns/plots] claim: ${cmd}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unclaimChunksForPlayer(plot, player) {
|
||||||
|
for (const [cx, cz] of plot.chunks) {
|
||||||
|
const cmd = `execute as ${player.username} in ${plot.dimension} run ftbchunks unclaim ${cx} ${cz}`;
|
||||||
|
Utils.server.runCommandSilent(cmd);
|
||||||
|
console.info(`[bns/plots] unclaim: ${cmd}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helpers ──────────────────────────────────────────────────────────
|
||||||
|
function formatPlotLine(plot) {
|
||||||
|
const owner = ownerOf(plot.id);
|
||||||
|
const status = owner ? `OWNED (${owner.substring(0,8)}...)` : 'AVAILABLE';
|
||||||
|
const cn = plot.chunks.length;
|
||||||
|
return `${plot.id} "${plot.name}" ${plot.size} ${cn} chunk${cn>1?'s':''} ${plot.price} spurs [${status}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMulti(source, lines) {
|
||||||
|
for (const line of lines) source.sendSystemMessage(Text.of(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Command tree ─────────────────────────────────────────────────────
|
||||||
|
ServerEvents.commandRegistry(event => {
|
||||||
|
const { commands: Commands, arguments: Arguments } = event;
|
||||||
|
|
||||||
|
const plotIdArg = () => Commands.argument('id', Arguments.STRING.create(event));
|
||||||
|
|
||||||
|
const tree = Commands.literal('bns')
|
||||||
|
.then(Commands.literal('plot')
|
||||||
|
|
||||||
|
// /bns plot list
|
||||||
|
.then(Commands.literal('list').executes(ctx => {
|
||||||
|
const lines = ['§7--- Spawn plots ---'];
|
||||||
|
for (const plot of PLOT_DEFS.values()) {
|
||||||
|
lines.push(formatPlotLine(plot));
|
||||||
|
}
|
||||||
|
lines.push('§7Run §f/bns plot info <id>§7 for details or §f/bns plot buy <id>§7 to purchase.');
|
||||||
|
sendMulti(ctx.source, lines);
|
||||||
|
return 1;
|
||||||
|
}))
|
||||||
|
|
||||||
|
// /bns plot info <id>
|
||||||
|
.then(Commands.literal('info').then(plotIdArg().executes(ctx => {
|
||||||
|
const id = Arguments.STRING.getResult(ctx, 'id');
|
||||||
|
const plot = PLOT_DEFS.get(id);
|
||||||
|
if (!plot) {
|
||||||
|
ctx.source.sendSystemMessage(Text.red(`No such plot: ${id}`));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const owner = ownerOf(id);
|
||||||
|
const chunkStr = plot.chunks.map(([x,z]) => `(${x},${z})`).join(', ');
|
||||||
|
sendMulti(ctx.source, [
|
||||||
|
`§6Plot ${id}: §f${plot.name}`,
|
||||||
|
`§7 Size: §f${plot.size} (${plot.chunks.length} chunk${plot.chunks.length>1?'s':''})`,
|
||||||
|
`§7 Chunks: §f${chunkStr}`,
|
||||||
|
`§7 Dimension: §f${plot.dimension}`,
|
||||||
|
`§7 Price: §f${plot.price} spurs`,
|
||||||
|
`§7 Owner: §f${owner || 'AVAILABLE'}`,
|
||||||
|
]);
|
||||||
|
return 1;
|
||||||
|
})))
|
||||||
|
|
||||||
|
// /bns plot buy <id>
|
||||||
|
.then(Commands.literal('buy').then(plotIdArg().executes(ctx => {
|
||||||
|
const player = ctx.source.player;
|
||||||
|
if (!player) {
|
||||||
|
ctx.source.sendSystemMessage(Text.red('Run from a player context.'));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const id = Arguments.STRING.getResult(ctx, 'id');
|
||||||
|
const plot = PLOT_DEFS.get(id);
|
||||||
|
if (!plot) {
|
||||||
|
player.tell(Text.red(`No such plot: ${id}`));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (ownerOf(id)) {
|
||||||
|
player.tell(Text.red(`Plot ${id} is already owned.`));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const existing = plotOwnedBy(player.uuid.toString());
|
||||||
|
if (existing) {
|
||||||
|
player.tell(Text.red(
|
||||||
|
`You already own plot ${existing}. Release it first with /bns plot release.`
|
||||||
|
));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const have = countCoins(player);
|
||||||
|
if (have < plot.price) {
|
||||||
|
player.tell(Text.red(
|
||||||
|
`You need ${plot.price} spurs (have ${have}). Earn coins at the bank exchange.`
|
||||||
|
));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const took = takeCoins(player, plot.price);
|
||||||
|
if (took < plot.price) {
|
||||||
|
// Shouldn't happen if countCoins agreed, but be safe.
|
||||||
|
giveCoins(player, took);
|
||||||
|
player.tell(Text.red(`Coin removal failed -- refunded. Try again.`));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
recordPurchase(id, player.uuid.toString());
|
||||||
|
claimChunksForPlayer(plot, player);
|
||||||
|
player.tell(Text.gold(
|
||||||
|
`Plot ${id} (${plot.name}) is now yours. Paid ${plot.price} spurs.`
|
||||||
|
));
|
||||||
|
console.info(`[bns/plots] ${player.username} bought ${id} for ${plot.price} spurs`);
|
||||||
|
return 1;
|
||||||
|
})))
|
||||||
|
|
||||||
|
// /bns plot release
|
||||||
|
.then(Commands.literal('release').executes(ctx => {
|
||||||
|
const player = ctx.source.player;
|
||||||
|
if (!player) {
|
||||||
|
ctx.source.sendSystemMessage(Text.red('Run from a player context.'));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const id = plotOwnedBy(player.uuid.toString());
|
||||||
|
if (!id) {
|
||||||
|
player.tell(Text.red(`You don't own a plot.`));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const plot = PLOT_DEFS.get(id);
|
||||||
|
const refund = Math.floor(plot.price * REFUND_FRACTION);
|
||||||
|
unclaimChunksForPlayer(plot, player);
|
||||||
|
clearOwner(id);
|
||||||
|
giveCoins(player, refund);
|
||||||
|
player.tell(Text.gold(
|
||||||
|
`Released plot ${id}. Refunded ${refund} spurs (${Math.round(REFUND_FRACTION*100)}% of ${plot.price}).`
|
||||||
|
));
|
||||||
|
console.info(`[bns/plots] ${player.username} released ${id}, refunded ${refund} spurs`);
|
||||||
|
return 1;
|
||||||
|
}))
|
||||||
|
|
||||||
|
// /bns plot reload (OP)
|
||||||
|
.then(Commands.literal('reload').requires(src => src.hasPermission(2))
|
||||||
|
.executes(ctx => {
|
||||||
|
// V1: PLOT_DEFS is hardcoded; reload is a no-op until V1.1
|
||||||
|
// (JSON-from-disk loading). Logged so admins know it ran.
|
||||||
|
ctx.source.sendSystemMessage(Text.gold(
|
||||||
|
`Plot defs are hardcoded in plots.js (V1). Edit the file + restart to change.`
|
||||||
|
));
|
||||||
|
console.info(`[bns/plots] reload called (no-op in V1)`);
|
||||||
|
return 1;
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
event.register(tree);
|
||||||
|
console.info('[bns/plots] commands registered: list, info, buy, release, reload');
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Brass & Sigil — Numismatics coin recipe removal
|
||||||
|
//
|
||||||
|
// The 5 coin denominations cannot be crafted by players. All currency
|
||||||
|
// enters circulation exclusively through the bank exchange (item-in /
|
||||||
|
// coin-out). Initially with fixed rates admin-configured on Numismatics
|
||||||
|
// vendor blocks; future dynamic-rates layer adjusts based on trade
|
||||||
|
// volume (see project-mc-player-economy.md).
|
||||||
|
//
|
||||||
|
// Other Numismatics items (piggy banks, bank tellers, vendor blocks,
|
||||||
|
// depositors, bank cards) stay craftable -- those are infrastructure,
|
||||||
|
// not currency.
|
||||||
|
|
||||||
|
const COIN_IDS = [
|
||||||
|
'numismatics:spur',
|
||||||
|
'numismatics:bevel',
|
||||||
|
'numismatics:cog',
|
||||||
|
'numismatics:crown',
|
||||||
|
'numismatics:sun',
|
||||||
|
];
|
||||||
|
|
||||||
|
ServerEvents.recipes(event => {
|
||||||
|
COIN_IDS.forEach(coin => event.remove({ output: coin }));
|
||||||
|
console.info(`[bns/recipes_numismatics] removed crafting recipes for ${COIN_IDS.length} coin denominations`);
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Brass & Sigil — Waystones recipe removal
|
||||||
|
//
|
||||||
|
// All Waystones items are uncraftable. Players never see waystones,
|
||||||
|
// sharestones, portstones, warp plates, warp scrolls or warp stones in
|
||||||
|
// the recipe book or JEI search results (see also jei_hides.js on the
|
||||||
|
// client side).
|
||||||
|
//
|
||||||
|
// Regular waystones are distributed exclusively via the first-join grant
|
||||||
|
// (welcome.js). Replacement waystones for players who break theirs will
|
||||||
|
// come from the bank vendor once the economy ships -- until then OPs
|
||||||
|
// hand them out via /give.
|
||||||
|
|
||||||
|
ServerEvents.recipes(event => {
|
||||||
|
event.remove({ mod: 'waystones' });
|
||||||
|
console.info('[bns/recipes_waystones] removed all waystones recipes');
|
||||||
|
});
|
||||||
@@ -62,6 +62,7 @@ BlockEvents.placed(event => {
|
|||||||
'That block is disabled. Use a regular Waystone (1 per player) ' +
|
'That block is disabled. Use a regular Waystone (1 per player) ' +
|
||||||
'and build transport between distant bases.'
|
'and build transport between distant bases.'
|
||||||
));
|
));
|
||||||
|
console.info(`[bns/waystones_policy] denied ${blockId} placement by ${player.username} (banned)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,9 +75,11 @@ BlockEvents.placed(event => {
|
|||||||
`You already have ${WAYSTONE_CAP} waystone. Break your existing one ` +
|
`You already have ${WAYSTONE_CAP} waystone. Break your existing one ` +
|
||||||
`to relocate, or build infrastructure to reach distant bases.`
|
`to relocate, or build infrastructure to reach distant bases.`
|
||||||
));
|
));
|
||||||
|
console.info(`[bns/waystones_policy] denied ${blockId} placement by ${player.username} (already at cap ${current})`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data.putInt('bnsWaystoneCount', current + 1);
|
data.putInt('bnsWaystoneCount', current + 1);
|
||||||
|
console.info(`[bns/waystones_policy] +1 waystone for ${player.username} (now ${current + 1})`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,5 +92,6 @@ BlockEvents.broken(event => {
|
|||||||
const current = data.getInt('bnsWaystoneCount');
|
const current = data.getInt('bnsWaystoneCount');
|
||||||
if (current > 0) {
|
if (current > 0) {
|
||||||
data.putInt('bnsWaystoneCount', current - 1);
|
data.putInt('bnsWaystoneCount', current - 1);
|
||||||
|
console.info(`[bns/waystones_policy] -1 waystone for ${player.username} (now ${current - 1})`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ PlayerEvents.loggedIn(event => {
|
|||||||
giveManual(player);
|
giveManual(player);
|
||||||
data.putBoolean(WELCOME_FLAG, true);
|
data.putBoolean(WELCOME_FLAG, true);
|
||||||
player.tell(Text.gold('Welcome to Brass & Sigil. Check your inventory for a Waystone and the server manual.'));
|
player.tell(Text.gold('Welcome to Brass & Sigil. Check your inventory for a Waystone and the server manual.'));
|
||||||
|
console.info(`[bns/welcome] granted starter packet to ${player.username}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If something goes wrong, don't set the flag -- player gets another chance.
|
// If something goes wrong, don't set the flag -- player gets another chance.
|
||||||
console.error(`[bns/welcome] failed for ${player.username}: ${e}`);
|
console.error(`[bns/welcome] failed for ${player.username}: ${e}`);
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "Brass-and-Sigil pack.lock.json - generated, do not edit by hand unless you know what you are doing",
|
"$schema": "Brass-and-Sigil pack.lock.json - generated, do not edit by hand unless you know what you are doing",
|
||||||
"name": "Brass and Sigil",
|
"name": "Brass and Sigil",
|
||||||
"version": "0.19.4",
|
"version": "0.21.2",
|
||||||
"minecraft": "1.21.1",
|
"minecraft": "1.21.1",
|
||||||
"loader": {
|
"loader": {
|
||||||
"type": "neoforge",
|
"type": "neoforge",
|
||||||
|
|||||||
Reference in New Issue
Block a user