Files
Samuel LisonandClaude Opus 4.8 177a4118fe feat(trade): secure player-to-player trading (#15)
A new server-authoritative, dupe-proof trade system. Walk up to another player,
trigger the "Trade" prompt; they Accept/Decline; both stage loose backpack
stacks (drag from the inventory grid, with −/+ qty steppers) and must Confirm
before anything moves.

Anti-dupe by construction: staging is by-reference (items never leave the owner
until commit), and the commit is one synchronous, no-yield critical section —
re-validate holds → pre-flight both receivers have room (new Inventory.canAccept)
→ remove both → grant with addUpTo → refund any residue. Item count is conserved
on every branch (verified: 200k-iteration conservation + fit-oracle fuzz).

Auto-cancels on death / leave / out-of-range (MaxDistance) / request timeout; a
staging change resets both confirms. New Trade server system + TradeUi client
window, the "Trading" Config section (tunable in SurvivorCore Studio), hooks
trade:started / trade:completed, and a trades_total progression counter. v1 is
backpack stacks only (worn gear reserved behind AllowEquippedItems).

- src/systems/Trade.luau, src/client/TradeUi.luau, src/shared/TradingConfig.luau (new)
- src/systems/Inventory.luau: exported canAccept (weight+slot fit oracle)
- src/shared/EngineConfig.luau: Trading section; src/init.luau boot wiring
- docs/trading.md, README, CHANGELOG (Unreleased), Hooks catalogue, extending.md
- demo/server/TradeTestStation.server.luau: /tradetest 2-player conservation harness

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 20:19:06 +10:00

65 lines
2.8 KiB
Luau

--[[
Hooks lifecycle extension points. Where EventBridge is "something happened",
Hooks is "the engine is about to / just did X — creators, do your thing here".
Hooks.on("craft:start", function(ctx) ctx.station:lightFire() end)
Hooks.on("craft:end", function(ctx) ringBell(ctx.station) end)
-- inside the engine:
Hooks.run("craft:start", { station = station, recipe = recipe, player = player })
This is how game-specific flourish (tree-felling physics, station VFX, custom drops)
stays OUT of the engine while remaining first-class.
Engine-fired hooks (ctx is a single table unless noted):
gather:hit / gather:depleted / gather:blocked { instance, player, resource, item?, }
craft:start / craft:end / craft:blocked { station?, recipe, player, reason? }
item:use { player, itemId, }
mob:spawned / mob:hit / mob:died { instance, mobType, player?, killer?, position? }
mob:attack { instance, mobType, player, damage, position }
combat:hit { attacker, victim, weapon, source, damage, victimHpLeft }
combat:kill { attacker, victim, weapon, source } -- source "melee"|"bow"
quest:started / quest:progress / quest:completed / quest:blocked
{ player, questId, def?, index?, count?, reason? }
achievement:unlocked { player, key, def }
player:died { player, position } -- after any death-drop
lootbag:dropped / lootbag:collected { player, bag, position?, items? / emptied }
trade:started / trade:completed { player, partner, gave?, got? } -- fired per player
Per-resource / per-mob-type variants of these dispatch through Reactions (see Reactions.luau):
SurvivorCore.Gather.onReaction(resourceId, ) and SurvivorCore.Mobs.onReaction(mobType, ).
Gameplay lifecycle events (gather:*, craft:*, item:use, mob:*, combat:*, quest:*,
achievement:unlocked) ALSO cross the EventBridge bus with the same names that is what
quests/achievements/analytics subscribe to (see EventBridge.luau and systems/Progression.luau).
]]
local Hooks = {}
type Hook = (...any) -> ()
local hooks: { [string]: { Hook } } = {}
function Hooks.on(name: string, callback: Hook): () -> ()
hooks[name] = hooks[name] or {}
table.insert(hooks[name], callback)
return function()
local list = hooks[name]
local i = list and table.find(list, callback)
if i then
table.remove(list, i)
end
end
end
function Hooks.run(name: string, ...)
local list = hooks[name]
if not list then
return
end
for _, cb in list do
task.spawn(cb, ...)
end
end
return Hooks