aboutsummaryrefslogtreecommitdiff
path: root/internal/world/hazard.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-24 20:58:25 -0400
committerhistoria <[not public]>2026-06-24 20:58:25 -0400
commit9d4b799db4868bcab291b83599ff088763cd4ed6 (patch)
tree252f0fcc779acf35243983d643d6399ee2e0cd0b /internal/world/hazard.go
parentd25638d98fe63efdeab570ef77e6a6a97f2d7a60 (diff)
downloadthehouseoficarus-9d4b799db4868bcab291b83599ff088763cd4ed6.tar.gz
feat: new type of non-violent 'combat': work
Diffstat (limited to 'internal/world/hazard.go')
-rw-r--r--internal/world/hazard.go82
1 files changed, 82 insertions, 0 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
+}