aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-01 16:37:45 -0400
committerhistoria <[not public]>2026-07-01 16:37:45 -0400
commite6b4cb9f5816c6ee239233bc85f74ee446a161c2 (patch)
tree1c3bf71c8b7f095baae989afe5e33183a7ed3929 /internal/world
parent649ef4b7416ce7053145878841fc9b0bff2f5fec (diff)
downloadthehouseoficarus-e6b4cb9f5816c6ee239233bc85f74ee446a161c2.tar.gz
feat: shop system overhauled, gui conversation tree editor overhauled
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/mob.go154
1 files changed, 146 insertions, 8 deletions
diff --git a/internal/world/mob.go b/internal/world/mob.go
index cc2d2b2..58bf769 100644
--- a/internal/world/mob.go
+++ b/internal/world/mob.go
@@ -68,10 +68,13 @@ type MobDef struct {
CompleteMessage string `yaml:"complete_message"` // shown on completion
Talk *behavior.TalkConfig `yaml:"talk,omitempty"`
+ Shop *behavior.ShopConfig `yaml:"shop,omitempty"`
}
func (d *MobDef) IsTalkable() bool { return d.Talk != nil }
+func (d *MobDef) HasShop() bool { return d.Shop != nil }
+
func (d *MobDef) IsTask() bool { return d.Kind == "task" }
type MobInstance struct {
@@ -128,17 +131,26 @@ type MobInstance struct {
TalkConfig *behavior.TalkConfig
- Owner string
- OwnerOnly bool
- SpawnedByTrigger bool
- DespawnOnLeave bool
- DespawnRooms []int
- DespawnTicks float64
- DespawnCounter int
+ // Shop is the (shared, read-only) shop config from the def. Per-instance
+ // live stock is tracked in ShopStock; ShopRestock holds per-item countdown
+ // timers. All three are only mutated under MobStore.mu.
+ Shop *behavior.ShopConfig
+ ShopStock map[string]int
+ ShopRestock map[string]int
+
+ Owner string
+ OwnerOnly bool
+ SpawnedByTrigger bool
+ DespawnOnLeave bool
+ DespawnRooms []int
+ DespawnTicks float64
+ DespawnCounter int
}
func (m *MobInstance) IsTalkable() bool { return m.TalkConfig != nil }
+func (m *MobInstance) HasShop() bool { return m.Shop != nil }
+
func (m *MobInstance) IsTask() bool { return m.Kind == "task" }
func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []int, wanderInterval float64) *MobInstance {
@@ -146,7 +158,7 @@ func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []in
if def.Protected && hp <= 0 {
hp = 1
}
- return &MobInstance{
+ inst := &MobInstance{
InstanceID: instanceID,
DefID: def.ID,
Name: def.Name,
@@ -191,6 +203,20 @@ func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []in
CompleteMessage: def.CompleteMessage,
TalkConfig: def.Talk,
}
+ if def.Shop != nil {
+ inst.Shop = def.Shop
+ inst.ShopStock = make(map[string]int)
+ inst.ShopRestock = make(map[string]int)
+ for i := range def.Shop.Items {
+ it := &def.Shop.Items[i]
+ if it.ItemID == "" {
+ continue
+ }
+ inst.ShopStock[it.ItemID] = it.Stock
+ inst.ShopRestock[it.ItemID] = def.Shop.RestockInterval(it)
+ }
+ }
+ return inst
}
const (
@@ -330,6 +356,9 @@ func (s *MobStore) Tick() {
defer s.mu.Unlock()
for _, inst := range s.instances {
+ if inst.Shop != nil {
+ s.tickShopLocked(inst)
+ }
if inst.HP <= 0 || inst.HP >= inst.MaxHP {
inst.regenerateTick = 0
continue
@@ -347,6 +376,115 @@ func (s *MobStore) Tick() {
}
}
+// tickShopLocked advances restock timers for one shop instance. Current stock
+// converges toward each item's target (configured stock for listed items, 0 for
+// dynamically-acquired items) one unit per restock interval, in either
+// direction. Caller must hold s.mu.
+func (s *MobStore) tickShopLocked(inst *MobInstance) {
+ for id, cur := range inst.ShopStock {
+ target := 0
+ var cfgItem *behavior.ShopItem
+ if cfgItem = inst.Shop.FindItem(id); cfgItem != nil {
+ target = cfgItem.Stock
+ }
+ if cur == target {
+ if cur == 0 && cfgItem == nil {
+ delete(inst.ShopStock, id)
+ delete(inst.ShopRestock, id)
+ }
+ continue
+ }
+ c := inst.ShopRestock[id]
+ if c <= 0 {
+ c = inst.Shop.RestockInterval(cfgItem)
+ }
+ c--
+ if c <= 0 {
+ if cur < target {
+ cur++
+ } else {
+ cur--
+ }
+ inst.ShopStock[id] = cur
+ c = inst.Shop.RestockInterval(cfgItem)
+ }
+ inst.ShopRestock[id] = c
+ if cur == 0 && cfgItem == nil {
+ delete(inst.ShopStock, id)
+ delete(inst.ShopRestock, id)
+ }
+ }
+}
+
+// ShopStockSnapshot returns a copy of the instance's current stock map.
+func (s *MobStore) ShopStockSnapshot(instanceID string) map[string]int {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ inst := s.instances[instanceID]
+ if inst == nil || inst.ShopStock == nil {
+ return nil
+ }
+ out := make(map[string]int, len(inst.ShopStock))
+ for k, v := range inst.ShopStock {
+ out[k] = v
+ }
+ return out
+}
+
+// ShopStockOf returns the current stock the instance holds of an item.
+func (s *MobStore) ShopStockOf(instanceID, itemID string) int {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ inst := s.instances[instanceID]
+ if inst == nil || inst.ShopStock == nil {
+ return 0
+ }
+ return inst.ShopStock[itemID]
+}
+
+// ShopTake removes up to qty of an item from the shop's stock, returning the
+// amount actually taken (capped by availability). Used when a player buys.
+func (s *MobStore) ShopTake(instanceID, itemID string, qty int) int {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ inst := s.instances[instanceID]
+ if inst == nil || inst.ShopStock == nil {
+ return 0
+ }
+ have := inst.ShopStock[itemID]
+ if qty > have {
+ qty = have
+ }
+ if qty <= 0 {
+ return 0
+ }
+ inst.ShopStock[itemID] = have - qty
+ return qty
+}
+
+// ShopAdd adds qty of an item to the shop's stock. Used when a player sells.
+func (s *MobStore) ShopAdd(instanceID, itemID string, qty int) {
+ if qty <= 0 {
+ return
+ }
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ inst := s.instances[instanceID]
+ if inst == nil || inst.Shop == nil {
+ return
+ }
+ if inst.ShopStock == nil {
+ inst.ShopStock = make(map[string]int)
+ }
+ if inst.ShopRestock == nil {
+ inst.ShopRestock = make(map[string]int)
+ }
+ if _, ok := inst.ShopStock[itemID]; !ok {
+ inst.ShopRestock[itemID] = inst.Shop.RestockInterval(inst.Shop.FindItem(itemID))
+ }
+ inst.ShopStock[itemID] += qty
+}
+
func (s *MobStore) RollIdleDescription(inst *MobInstance) {
def, err := s.LoadDef(inst.DefID)
if err != nil {