diff options
Diffstat (limited to 'internal/world')
| -rw-r--r-- | internal/world/hazard.go | 82 | ||||
| -rw-r--r-- | internal/world/mob.go | 23 | ||||
| -rw-r--r-- | internal/world/objects.go | 21 | ||||
| -rw-r--r-- | internal/world/room.go | 24 | ||||
| -rw-r--r-- | internal/world/world.go | 5 |
5 files changed, 151 insertions, 4 deletions
diff --git a/internal/world/hazard.go b/internal/world/hazard.go new file mode 100644 index 0000000..396343b --- /dev/null +++ b/internal/world/hazard.go @@ -0,0 +1,82 @@ +package world + +import ( + "fmt" + "os" + "path/filepath" + + "thehouseoficarus/internal/action" + "gopkg.in/yaml.v3" +) + +// HazardDef describes an environmental threat attached to a room (a sandstorm, +// solar radiation, falling rocks, etc.). It is a shared, ID-referenced +// definition loaded from data/hazards/<id>.yaml, so one hazard can be reused +// across a whole area and resolved by name for a future `examine` command. +// +// A hazard rolls an attack against everyone in the room every Speed ticks using +// the SAME combat formulas as a mob: accuracy comes from Attack/AttackBonus, max +// hit from MaxHit, and the player defends with their Defense level + equipment +// bonus selected by AttackType (so existing armour and the Defense skill apply). +// Tech protection (Kinetic Barrier / Projectile Screen / Neural Firewall) keys +// off AttackType automatically. +type HazardDef struct { + ID string `yaml:"id"` + Name string `yaml:"name"` + Description string `yaml:"description"` + AttackType string `yaml:"attack_type"` // stab/slash/crush/ranged/science + Attack int `yaml:"attack"` + AttackBonus int `yaml:"attack_bonus"` + MaxHit int `yaml:"max_hit"` + Speed float64 `yaml:"speed"` // ticks between hazard rolls + WarnMessage string `yaml:"warn_message"` + HitMessage string `yaml:"hit_message"` // accepts a single %d for damage + MissMessage string `yaml:"miss_message"` + RequiredItem string `yaml:"required_item"` // equipped item that negates the hazard +} + +// LoadHazard returns the shared hazard definition for id, caching results. +func (w *World) LoadHazard(id string) (*HazardDef, error) { + w.hazardMu.Lock() + if w.hazardPathIndex == nil { + w.hazardPathIndex = action.BuildPathIndex(filepath.Join(w.dataDir, "hazards")) + } + if def, ok := w.hazardDefs[id]; ok { + w.hazardMu.Unlock() + return def, nil + } + path, ok := w.hazardPathIndex[id] + w.hazardMu.Unlock() + if !ok { + return nil, fmt.Errorf("read hazard %s: no such hazard", id) + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read hazard %s: %w", id, err) + } + var def HazardDef + if err := yaml.Unmarshal(data, &def); err != nil { + return nil, fmt.Errorf("parse hazard %s: %w", id, err) + } + def.ID = id + + w.hazardMu.Lock() + w.hazardDefs[id] = &def + w.hazardMu.Unlock() + return &def, nil +} + +// HazardIndex returns the set of known hazard IDs (used by startup validation). +func (w *World) HazardIndex() map[string]bool { + w.hazardMu.Lock() + defer w.hazardMu.Unlock() + if w.hazardPathIndex == nil { + w.hazardPathIndex = action.BuildPathIndex(filepath.Join(w.dataDir, "hazards")) + } + ids := make(map[string]bool, len(w.hazardPathIndex)) + for id := range w.hazardPathIndex { + ids[id] = true + } + return ids +} diff --git a/internal/world/mob.go b/internal/world/mob.go index ec7db36..4864e5c 100644 --- a/internal/world/mob.go +++ b/internal/world/mob.go @@ -58,11 +58,23 @@ type MobDef struct { DamageWithout string `yaml:"damage_without"` Size string `yaml:"size"` + // Kind discriminates a normal combat mob ("" or "combat") from a "task" + // worksite (build a solar panel, survey a rock, etc.). A task mob reuses the + // entire combat engine but its HP drains to 0 to COMPLETE the work; it never + // attacks back (the room hazard, if any, supplies the danger). The progress + // bar is displayed inverted (filling toward 100%). + Kind string `yaml:"kind"` + Verb string `yaml:"verb"` // flavor verb for messages (default "work") + ProgressNoun string `yaml:"progress_noun"` // flavor noun, e.g. "construction" + CompleteMessage string `yaml:"complete_message"` // shown on completion + Talk *action.TalkConfig `yaml:"talk,omitempty"` } func (d *MobDef) IsTalkable() bool { return d.Talk != nil } +func (d *MobDef) IsTask() bool { return d.Kind == "task" } + type MobInstance struct { InstanceID string DefID string @@ -109,11 +121,18 @@ type MobInstance struct { DamageWithout string Size string + Kind string + Verb string + ProgressNoun string + CompleteMessage string + TalkConfig *action.TalkConfig } func (m *MobInstance) IsTalkable() bool { return m.TalkConfig != nil } +func (m *MobInstance) IsTask() bool { return m.Kind == "task" } + func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []int, wanderInterval float64) *MobInstance { return &MobInstance{ InstanceID: instanceID, @@ -153,6 +172,10 @@ func NewMobInstance(def *MobDef, instanceID string, roomID int, wanderRooms []in FinishingBlow: def.FinishingBlow, DamageWithout: def.DamageWithout, Size: def.Size, + Kind: def.Kind, + Verb: def.Verb, + ProgressNoun: def.ProgressNoun, + CompleteMessage: def.CompleteMessage, TalkConfig: def.Talk, } } diff --git a/internal/world/objects.go b/internal/world/objects.go index bf4b592..a7e0605 100644 --- a/internal/world/objects.go +++ b/internal/world/objects.go @@ -14,6 +14,7 @@ type ObjState struct { DepleteTimer float64 DefID string Name string + Aliases []string Index int RoomID int JustRespawned bool @@ -160,12 +161,13 @@ func (w *World) FindObjInstances(roomID int, name string) []ObjState { if st.RoomID != roomID { continue } - if !wordMatchesObj(lower, st.DefID, st.Name) { + if !wordMatchesObj(lower, st.DefID, st.Name, st.Aliases) { continue } out = append(out, ObjState{ DefID: st.DefID, Name: st.Name, + Aliases: st.Aliases, Index: st.Index, RoomID: st.RoomID, Depleted: st.Depleted, @@ -184,10 +186,15 @@ func (w *World) FindObjInstances(roomID int, name string) []ObjState { return out } -func wordMatchesObj(lower, defID, objName string) bool { +func wordMatchesObj(lower, defID, objName string, aliases []string) bool { if object.WordPrefixMatch(lower, objName) { return true } + for _, alias := range aliases { + if object.WordPrefixMatch(lower, alias) { + return true + } + } inputWords := strings.Fields(strings.ToLower(lower)) if strings.HasPrefix(strings.ToLower(defID), lower) || strings.HasPrefix(lower, strings.ToLower(defID)) { @@ -214,6 +221,16 @@ func (w *World) SetObjName(roomID int, defID string, name string) { } } +func (w *World) SetObjAliases(roomID int, defID string, aliases []string) { + w.mu.Lock() + defer w.mu.Unlock() + for _, st := range w.objStates { + if st.RoomID == roomID && st.DefID == defID { + st.Aliases = aliases + } + } +} + func (w *World) SetObjWander(roomID int, defID string, rooms []int, interval float64) { w.mu.Lock() defer w.mu.Unlock() diff --git a/internal/world/room.go b/internal/world/room.go index 752ec41..2441c30 100644 --- a/internal/world/room.go +++ b/internal/world/room.go @@ -48,6 +48,8 @@ type ExitDef struct { Room int `yaml:"room"` Condition *action.Condition `yaml:"condition"` BlockedMessage string `yaml:"blocked_message"` + SetFlags map[string]any `yaml:"set_flags"` + SetPlayerFlags map[string]any `yaml:"set_player_flags"` } func (e *ExitDef) UnmarshalYAML(value *yaml.Node) error { @@ -67,11 +69,18 @@ type Room struct { ID int `yaml:"id"` Name string `yaml:"name"` Description string `yaml:"description"` + Descriptions []RoomDesc `yaml:"descriptions,omitempty"` Exits map[ExitDir]ExitDef `yaml:"exits"` Objects []RoomObject `yaml:"objects"` ItemSpawns []SpawnDef `yaml:"item_spawns"` Mobs []RoomMob `yaml:"mobs"` OnEnter []EnterStep `yaml:"on_enter"` + Hazard string `yaml:"hazard"` +} + +type RoomDesc struct { + Text string `yaml:"text"` + Condition *action.Condition `yaml:"condition"` } type RoomMob struct { @@ -94,8 +103,19 @@ func (rm *RoomMob) UnmarshalYAML(value *yaml.Node) error { } type EnterStep struct { - Message string `yaml:"message"` - Condition *action.Condition `yaml:"condition"` + Message string `yaml:"message"` + Condition *action.Condition `yaml:"condition"` + Delay int `yaml:"delay"` + SetFlags map[string]any `yaml:"set_flags"` + SetPlayerFlags map[string]any `yaml:"set_player_flags"` +} + +// IsTimed reports whether the step carries cutscene semantics: a tick delay or +// a flag mutation. A room whose qualifying on_enter steps are all non-timed is +// printed synchronously; any timed step turns the sequence into a scheduled +// cutscene. +func (e EnterStep) IsTimed() bool { + return e.Delay > 0 || len(e.SetFlags) > 0 || len(e.SetPlayerFlags) > 0 } type RoomObject struct { diff --git a/internal/world/world.go b/internal/world/world.go index b305c4b..8ed9a7c 100644 --- a/internal/world/world.go +++ b/internal/world/world.go @@ -19,6 +19,10 @@ type World struct { seeded map[int]bool objStates map[string]*ObjState objMoves []ObjMove + + hazardMu sync.Mutex + hazardPathIndex map[string]string + hazardDefs map[string]*HazardDef } func New(dataDir string) *World { @@ -28,6 +32,7 @@ func New(dataDir string) *World { groundItems: make(map[int][]*groundEntry), seeded: make(map[int]bool), objStates: make(map[string]*ObjState), + hazardDefs: make(map[string]*HazardDef), } } |
