From d02f0d2cdf67f1445a9ccae86b99a2578adab585 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 10 Jul 2026 17:02:13 -0400 Subject: feat(admin): update search to use standard condition/action framework rather than bespoke implementation --- internal/game/act.go | 2 - internal/game/act_effects.go | 75 +++++++++++++++++++++++++----- internal/game/act_search.go | 107 ++----------------------------------------- internal/game/act_state.go | 2 - internal/game/cmd_search.go | 3 +- 5 files changed, 68 insertions(+), 121 deletions(-) (limited to 'internal/game') diff --git a/internal/game/act.go b/internal/game/act.go index 352f235..64d1848 100644 --- a/internal/game/act.go +++ b/internal/game/act.go @@ -213,8 +213,6 @@ func (g *Game) AdvanceActions() { g.advanceBurn(sess, p) case "stoke": g.advanceStoke(sess, p) - case "search": - g.advanceSearch(sess, p) case "identify": g.advanceIdentify(sess, p) case "steal": diff --git a/internal/game/act_effects.go b/internal/game/act_effects.go index cb60c5d..cadd05e 100644 --- a/internal/game/act_effects.go +++ b/internal/game/act_effects.go @@ -44,12 +44,14 @@ const ( // 5. set_player_flags (player scope; cascades via setPlayerFlag) // 6. take_item (player scope) // 7. give_item (player scope; honors 28-slot inventory limit) -// 8. heal (player scope; clamps to MaxHP) -// 9. credits (player scope; negative is gated by affordability) -// 10. spawn_mob (player scope: player-owned; global scope: world-owned) -// 11. despawn_mob (player scope: owner-filtered; global scope: all) -// 12. teleport (player scope; re-runs look + on_enter of target) -// 13. aps_node (player scope; marks "aps_node_" player flag) +// 8. drop_table (player scope; resolves weighted drops into inventory, +// overflow to ground; supports credits pseudo-item) +// 9. heal (player scope; clamps to MaxHP) +// 10. credits (player scope; negative is gated by affordability) +// 11. spawn_mob (player scope: player-owned; global scope: world-owned) +// 12. despawn_mob (player scope: owner-filtered; global scope: all) +// 13. teleport (player scope; re-runs look + on_enter of target) +// 14. aps_node (player scope; marks "aps_node_" player flag) func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Step, roomID int, sc effectScope, flagValue any) { if step == nil { return @@ -97,7 +99,7 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste } if sc == scopeGlobal { - // 10/11. world-owned spawn/despawn. + // 11/12. world-owned spawn/despawn. if step.SpawnMob != nil { g.spawnWorldTriggerMob(step.SpawnMob, roomID) } @@ -138,7 +140,17 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste } } - // 8. heal + // 8. drop_table — weighted loot into inventory, overflow to ground. + if len(step.DropTable) > 0 { + for _, d := range behavior.ResolveDropList(g.DataDir, step.DropTable) { + if d.ItemID == "" { + continue + } + g.giveTriggerDrop(sess, p, &d) + } + } + + // 9. heal if step.Heal > 0 { p.HP += step.Heal if maxHP := p.MaxHP(); p.HP > maxHP { @@ -150,7 +162,7 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste } } - // 9. credits (signed; negative is gated). + // 10. credits (signed; negative is gated). if step.Credits != 0 { if step.Credits < 0 { if p.Credits >= -step.Credits { @@ -165,7 +177,7 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste } } - // 10/11. spawn_mob / despawn_mob (player-owned / owner-filtered). + // 11/12. spawn_mob / despawn_mob (player-owned / owner-filtered). if step.SpawnMob != nil { g.spawnTriggerMob(sess, p, step.SpawnMob, roomID) } @@ -173,12 +185,12 @@ func (g *Game) applyStep(sess *net.Session, p *player.Player, step *behavior.Ste g.despawnTriggerMobs(step.DespawnMob, p.Name) } - // 12. teleport + // 13. teleport if step.Teleport > 0 && sess != nil { g.teleportPlayer(sess, p, step.Teleport) } - // 13. aps_node — dynamic room-id flag, kept as a dedicated effect. + // 14. aps_node — dynamic room-id flag, kept as a dedicated effect. if step.ApsNode { g.setPlayerFlag(p, "aps_node_"+strconv.Itoa(p.RoomID), true) g.AccountStore.SaveCharacter(p) @@ -200,6 +212,45 @@ func (g *Game) writePlayerMessage(sess *net.Session, msg string, playerName stri sess.WriteLine(rendered) } +// giveTriggerDrop awards a resolved DropEntry to a player. Item goes to the +// first free inventory slot; if inventory is full it falls to the ground +// (reserved for the player). "credits" pseudo-item adds credits directly. +func (g *Game) giveTriggerDrop(sess *net.Session, p *player.Player, drop *behavior.DropEntry) { + if p == nil { + return + } + qty := drop.Quantity + if qty <= 0 { + qty = 1 + } + + if drop.ItemID == "credits" { + p.Credits += qty + if sess != nil { + sess.WriteLine(fmt.Sprintf("You receive %d credits.", qty)) + } + g.AccountStore.SaveCharacter(p) + return + } + + name := drop.ItemID + lootDef, _ := g.ItemStore.Load(drop.ItemID) + if lootDef != nil && lootDef.Name != "" { + name = lootDef.Name + } + + freeSlot := p.FirstFreeSlot() + if freeSlot == -1 { + g.World.AddReservedItem(p.RoomID, drop.ItemID, qty, p.Name) + sess.WriteLine(fmt.Sprintf("You receive %s. It falls to the ground.", g.itemColorize(sess, lootDef, name))) + return + } + + p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) + sess.WriteLine(fmt.Sprintf("You receive %s.", g.itemColorize(sess, lootDef, name))) + g.AccountStore.SaveCharacter(p) +} + // applyTalkStep fires a talk node/option's Action (a single Step) // synchronously at scopePlayer, scoped to the player's current room. func (g *Game) applyTalkStep(sess *net.Session, step *behavior.Step) { diff --git a/internal/game/act_search.go b/internal/game/act_search.go index 83ea534..430b265 100644 --- a/internal/game/act_search.go +++ b/internal/game/act_search.go @@ -1,116 +1,17 @@ package game import ( - "fmt" - - "thehouseoficarus/internal/behavior" - "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) -func (g *Game) startSearch(sess *net.Session, p *player.Player, itemID string, slotIdx int) { +func (g *Game) doSearchItem(sess *net.Session, p *player.Player, itemID string) { def, err := g.ItemStore.Load(itemID) - if err != nil || def.SearchTable == "" { + if err != nil || len(def.Search) == 0 { sess.WriteLine("You can't search that.") return } - - ticks := def.SearchTicks - if ticks <= 0 { - ticks = 3 - } - - p.Action = &behavior.Action{ - Type: "search", - TargetID: itemID, - TargetName: def.Name, - WaitLeft: engine.ToTicks(ticks), - Data: &behavior.SearchData{ - ItemID: itemID, - SlotIdx: slotIdx, - Started: false, - }, - } - - g.broadcastAction(sess, "%s searches a %s.", p.Name, def.Name) -} - -func (g *Game) advanceSearch(sess *net.Session, p *player.Player) { - d, ok := p.Action.Data.(*behavior.SearchData) - if !ok || d == nil { - sess.WriteLine("Something went wrong.") - g.cancelAction(p) - return - } - - def, err := g.ItemStore.Load(d.ItemID) - if err != nil { - sess.WriteLine("Something went wrong.") - g.cancelAction(p) - return - } - - if !d.Started { - msg := def.SearchMessage - if msg == "" { - msg = fmt.Sprintf("digging through the %s", def.Name) - } - sess.WriteLine(fmt.Sprintf("You begin %s.", msg)) - d.Started = true - return - } - - slot := p.InvSlot(d.SlotIdx) - if slot == nil || slot.ItemID != d.ItemID || slot.Quantity <= 0 { - sess.WriteLine("The item is gone.") - g.cancelAction(p) - return - } - - if slot.Quantity > 1 { - slot.Quantity-- - } else { - p.SetInvSlot(d.SlotIdx, nil) - } - - dt, err := behavior.LoadDropTable(g.DataDir, def.SearchTable) - if err == nil && len(dt.Drops) > 0 { - drop := behavior.ResolveDrop(g.DataDir, dt.Drops) - if drop != nil && drop.ItemID != "" { - g.giveSearchLoot(sess, p, drop) - } - } - - g.AccountStore.SaveCharacter(p) - g.cancelAction(p) -} - -func (g *Game) giveSearchLoot(sess *net.Session, p *player.Player, drop *behavior.DropEntry) { - qty := drop.Quantity - if qty <= 0 { - qty = 1 - } - - name := drop.ItemID - lootDef, _ := g.ItemStore.Load(drop.ItemID) - if lootDef != nil { - name = lootDef.Name - } - - if drop.ItemID == "credits" { - p.Credits += qty - sess.WriteLine(g.colorize(sess, "credits_pickup", fmt.Sprintf("You find %d credits.", qty))) - return - } - - freeSlot := p.FirstFreeSlot() - if freeSlot == -1 { - g.World.AddGroundItem(p.RoomID, drop.ItemID, qty) - sess.WriteLine(fmt.Sprintf("You find %s. It falls to the ground.", g.itemColorize(sess, lootDef, name))) - return + if !g.runTrigger(sess, p, def.Search, p.RoomID, p.HasItem, false) { + sess.WriteLine("You can't search that.") } - - p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) - sess.WriteLine(fmt.Sprintf("You find %s.", g.itemColorize(sess, lootDef, name))) } diff --git a/internal/game/act_state.go b/internal/game/act_state.go index 73ecd57..335ecfb 100644 --- a/internal/game/act_state.go +++ b/internal/game/act_state.go @@ -49,8 +49,6 @@ func (g *Game) playerActionDisplay(p *player.Player) string { return "trying to start a fire" case behavior.TypeStoke: return "tending to a fire" - case behavior.TypeSearch: - return "digging through a " + a.TargetName case behavior.TypeIdentify: return "identifying scrap at " + a.TargetName case behavior.TypePlant: diff --git a/internal/game/cmd_search.go b/internal/game/cmd_search.go index 4122e0a..2c5017e 100644 --- a/internal/game/cmd_search.go +++ b/internal/game/cmd_search.go @@ -40,6 +40,5 @@ func (g *Game) doSearch(sess *net.Session, input string) { } itemID := matches[0].ID - slotIdx := matches[0].Slot - g.startSearch(sess, p, itemID, slotIdx) + g.doSearchItem(sess, p, itemID) } -- cgit v1.2.3