diff options
| author | historia <[not public]> | 2026-06-09 05:59:20 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-09 05:59:20 -0400 |
| commit | 9eb5ab1818b6b19501fd7209db1987c9e06cc919 (patch) | |
| tree | 76e046b0bf611dfe939a8c8f6507467de2e9e20f /internal/world | |
| download | thehouseoficarus-9eb5ab1818b6b19501fd7209db1987c9e06cc919.tar.gz | |
first commit
Diffstat (limited to 'internal/world')
| -rw-r--r-- | internal/world/mob.go | 181 | ||||
| -rw-r--r-- | internal/world/room.go | 53 | ||||
| -rw-r--r-- | internal/world/world.go | 182 |
3 files changed, 416 insertions, 0 deletions
diff --git a/internal/world/mob.go b/internal/world/mob.go new file mode 100644 index 0000000..601cdc1 --- /dev/null +++ b/internal/world/mob.go @@ -0,0 +1,181 @@ +package world + +import ( + "fmt" + "os" + "path/filepath" + "sync" + + "gopkg.in/yaml.v3" +) + +type LootEntry struct { + ItemID string `yaml:"item_id"` + Weight int `yaml:"weight"` + Quantity int `yaml:"quantity"` +} + +type DropTable struct { + Remains string `yaml:"remains"` + Loot []LootEntry `yaml:"loot"` +} + +type MobDef struct { + ID string `yaml:"id"` + Name string `yaml:"name"` + Attack int `yaml:"attack"` + Strength int `yaml:"strength"` + Defense int `yaml:"defense"` + HP int `yaml:"hp"` + Speed int `yaml:"speed"` + Aggressive bool `yaml:"aggressive"` + RespawnTicks int `yaml:"respawn_ticks"` + Drops DropTable `yaml:"drops"` +} + +type MobInstance struct { + InstanceID string + DefID string + Name string + HP int + MaxHP int + Attack int + Strength int + Defense int + Speed int + Aggressive bool + RespawnTicks int + RoomID int + Drops DropTable +} + +type MobStore struct { + dataDir string + mu sync.Mutex + defs map[string]*MobDef + instances map[string]*MobInstance +} + +func NewMobStore(dataDir string) *MobStore { + return &MobStore{ + dataDir: dataDir, + defs: make(map[string]*MobDef), + instances: make(map[string]*MobInstance), + } +} + +func (s *MobStore) LoadDef(id string) (*MobDef, error) { + s.mu.Lock() + if def, ok := s.defs[id]; ok { + s.mu.Unlock() + return def, nil + } + s.mu.Unlock() + + path := filepath.Join(s.dataDir, "mobs", id+".yaml") + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read mob %s: %w", id, err) + } + var def MobDef + if err := yaml.Unmarshal(data, &def); err != nil { + return nil, fmt.Errorf("parse mob %s: %w", id, err) + } + + s.mu.Lock() + s.defs[id] = &def + s.mu.Unlock() + return &def, nil +} + +func (s *MobStore) SpawnMob(defID string, roomID int, instanceID string) (*MobInstance, error) { + def, err := s.LoadDef(defID) + if err != nil { + return nil, err + } + s.mu.Lock() + defer s.mu.Unlock() + inst := &MobInstance{ + InstanceID: instanceID, + DefID: defID, + Name: def.Name, + HP: def.HP, + MaxHP: def.HP, + Attack: def.Attack, + Strength: def.Strength, + Defense: def.Defense, + Speed: def.Speed, + Aggressive: def.Aggressive, + RespawnTicks: def.RespawnTicks, + RoomID: roomID, + Drops: def.Drops, + } + s.instances[instanceID] = inst + return inst, nil +} + +func (s *MobStore) GetInstance(id string) *MobInstance { + s.mu.Lock() + defer s.mu.Unlock() + return s.instances[id] +} + +func (s *MobStore) RemoveInstance(id string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.instances, id) +} + +func (s *MobStore) MobsInRoom(roomID int) []*MobInstance { + s.mu.Lock() + defer s.mu.Unlock() + var out []*MobInstance + for _, inst := range s.instances { + if inst.RoomID == roomID && inst.HP > 0 { + out = append(out, inst) + } + } + return out +} + +func (s *MobStore) SeedMobs(roomID int, mobIDs []string) { + type defWrapper struct { + def *MobDef + err error + } + defs := make([]defWrapper, len(mobIDs)) + for i, defID := range mobIDs { + d, err := s.LoadDef(defID) + defs[i] = defWrapper{d, err} + } + + s.mu.Lock() + defer s.mu.Unlock() + for i, dw := range defs { + if dw.err != nil { + continue + } + defID := mobIDs[i] + instID := fmt.Sprintf("%s_%d_%d", defID, roomID, i) + if inst, exists := s.instances[instID]; exists { + // Already exists — skip (respawn is handled by timers) + _ = inst + continue + } + s.instances[instID] = &MobInstance{ + InstanceID: instID, + DefID: defID, + Name: dw.def.Name, + HP: dw.def.HP, + MaxHP: dw.def.HP, + Attack: dw.def.Attack, + Strength: dw.def.Strength, + Defense: dw.def.Defense, + Speed: dw.def.Speed, + Aggressive: dw.def.Aggressive, + RespawnTicks: dw.def.RespawnTicks, + RoomID: roomID, + Drops: dw.def.Drops, + } + } +} diff --git a/internal/world/room.go b/internal/world/room.go new file mode 100644 index 0000000..a92a428 --- /dev/null +++ b/internal/world/room.go @@ -0,0 +1,53 @@ +package world + +type ExitDir string + +const ( + North ExitDir = "north" + South ExitDir = "south" + East ExitDir = "east" + West ExitDir = "west" + Up ExitDir = "up" + Down ExitDir = "down" +) + +var ExitAliases = map[string]ExitDir{ + "n": North, + "s": South, + "e": East, + "w": West, + "u": Up, + "d": Down, +} + +var OppositeExit = map[ExitDir]ExitDir{ + North: South, + South: North, + East: West, + West: East, + Up: Down, + Down: Up, +} + +var ExitOrder = []ExitDir{ + "northwest", North, "northeast", + East, "southeast", South, "southwest", + West, Up, Down, +} + +type SpawnDef struct { + ItemID string `yaml:"item_id"` + Quantity int `yaml:"quantity"` + RespawnTicks int `yaml:"respawn_ticks"` +} + +type Room struct { + ID int `yaml:"id"` + Name string `yaml:"name"` + Description string `yaml:"description"` + MapSymbol string `yaml:"map_symbol"` + Exits map[ExitDir]int `yaml:"exits"` + Objects []string `yaml:"objects"` + Spawns []SpawnDef `yaml:"spawns"` + Mobs []string `yaml:"mobs"` +} diff --git a/internal/world/world.go b/internal/world/world.go new file mode 100644 index 0000000..265a2f9 --- /dev/null +++ b/internal/world/world.go @@ -0,0 +1,182 @@ +package world + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "gopkg.in/yaml.v3" +) + +const DropDespawnTicks = 1000 + +type groundEntry struct { + itemID string + quantity int + isSpawn bool + respawnTimer int // >0 = counting down to respawn + respawnQty int + respawnDelay int + despawnTimer int // >0 = counting down to despawn (dropped items) +} + +type World struct { + dataDir string + mu sync.Mutex + groundItems map[int][]*groundEntry + seeded map[int]bool +} + +func New(dataDir string) *World { + return &World{ + dataDir: dataDir, + groundItems: make(map[int][]*groundEntry), + seeded: make(map[int]bool), + } +} + +func (w *World) LoadRoom(id int) (*Room, error) { + path := filepath.Join(w.dataDir, "rooms", fmt.Sprintf("%d.yaml", id)) + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read room %d: %w", id, err) + } + var room Room + if err := yaml.Unmarshal(data, &room); err != nil { + return nil, fmt.Errorf("parse room %d: %w", id, err) + } + room.ID = id + if room.Exits == nil { + room.Exits = make(map[ExitDir]int) + } + if room.Objects == nil { + room.Objects = make([]string, 0) + } + if room.Spawns == nil { + room.Spawns = make([]SpawnDef, 0) + } + if room.Mobs == nil { + room.Mobs = make([]string, 0) + } + return &room, nil +} + +func (w *World) ResolveExit(input string) ExitDir { + if dir, ok := ExitAliases[strings.ToLower(input)]; ok { + return dir + } + canon := ExitDir(strings.ToLower(input)) + switch canon { + case North, South, East, West, Up, Down: + return canon + } + return "" +} + +func (w *World) GroundItems(roomID int) map[string]int { + w.mu.Lock() + defer w.mu.Unlock() + out := make(map[string]int) + for _, e := range w.groundItems[roomID] { + if e.quantity > 0 { + out[e.itemID] += e.quantity + } + } + return out +} + +func (w *World) AddGroundItem(roomID int, itemID string, qty int) { + w.mu.Lock() + defer w.mu.Unlock() + e := &groundEntry{ + itemID: itemID, + quantity: qty, + despawnTimer: DropDespawnTicks, + } + w.groundItems[roomID] = append(w.groundItems[roomID], e) +} + +func (w *World) RemoveGroundItem(roomID int, itemID string, qty int) int { + w.mu.Lock() + defer w.mu.Unlock() + + removed := 0 + remaining := qty + + for _, e := range w.groundItems[roomID] { + if remaining <= 0 { + break + } + if !strings.EqualFold(e.itemID, itemID) { + continue + } + if e.quantity <= 0 { + continue + } + take := e.quantity + if take > remaining { + take = remaining + } + e.quantity -= take + removed += take + remaining -= take + + if e.isSpawn && e.quantity <= 0 && e.respawnDelay > 0 { + e.respawnTimer = e.respawnDelay + } + } + return removed +} + +func (w *World) SeedGroundItems(roomID int) { + w.mu.Lock() + if w.seeded[roomID] { + w.mu.Unlock() + return + } + w.seeded[roomID] = true + w.mu.Unlock() + + room, err := w.LoadRoom(roomID) + if err != nil { + return + } + + w.mu.Lock() + defer w.mu.Unlock() + + for _, s := range room.Spawns { + e := &groundEntry{ + itemID: s.ItemID, + quantity: s.Quantity, + isSpawn: true, + respawnDelay: s.RespawnTicks, + respawnQty: s.Quantity, + } + w.groundItems[roomID] = append(w.groundItems[roomID], e) + } +} + +func (w *World) Tick() { + w.mu.Lock() + defer w.mu.Unlock() + + for _, entries := range w.groundItems { + for _, e := range entries { + if e.respawnTimer > 0 { + e.respawnTimer-- + if e.respawnTimer <= 0 { + e.quantity = e.respawnQty + } + } + if e.despawnTimer > 0 { + e.despawnTimer-- + if e.despawnTimer <= 0 { + e.quantity = 0 + } + } + } + } +} |
