aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-09 05:59:20 -0400
committerhistoria <[not public]>2026-06-09 05:59:20 -0400
commit9eb5ab1818b6b19501fd7209db1987c9e06cc919 (patch)
tree76e046b0bf611dfe939a8c8f6507467de2e9e20f /internal/world
downloadthehouseoficarus-9eb5ab1818b6b19501fd7209db1987c9e06cc919.tar.gz
first commit
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/mob.go181
-rw-r--r--internal/world/room.go53
-rw-r--r--internal/world/world.go182
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
+ }
+ }
+ }
+ }
+}