1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
|
package world
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
"thehouseoficarus/internal/behavior"
"thehouseoficarus/internal/combat"
)
type HazardCombatBonuses struct {
AttackBonus int `yaml:"attack_bonus"`
StrengthBonus int `yaml:"strength_bonus"`
RangedBonus int `yaml:"ranged_bonus"`
RangedStrengthBonus int `yaml:"ranged_strength_bonus"`
ScienceBonus int `yaml:"science_bonus"`
SciencePercentBonus int `yaml:"science_percent_bonus"`
}
type HazardCombatStats struct {
Attack int `yaml:"attack"`
Strength int `yaml:"strength"`
Ranged int `yaml:"ranged"`
Science int `yaml:"science"`
Speed float64 `yaml:"speed"`
MaxMeleeHit int `yaml:"max_melee_hit"`
MaxRangedHit int `yaml:"max_ranged_hit"`
MaxScienceHit int `yaml:"max_science_hit"`
Bonuses HazardCombatBonuses `yaml:"bonuses"`
}
type HazardCombat struct {
AttackTypes []string `yaml:"attack_types"`
Stats HazardCombatStats `yaml:"stats"`
}
// HazardDef describes an environmental threat attached to a room. Its combat
// block mirrors a mob's combat block (attack_types, stats with per-type max hits
// and bonuses) but has NO defense stats — the hazard is never attacked, it only
// attacks the player. Speed is read from combat.stats.speed (default 5).
type HazardDef struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Combat *HazardCombat `yaml:"combat,omitempty"`
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
}
// AttackType returns the hazard's active attack type: the first melee type from
// attack_types, falling back to the first entry, defaulting to crush.
func (d *HazardDef) AttackType() string {
if d.Combat == nil || len(d.Combat.AttackTypes) == 0 {
return combat.DefaultAttackType
}
for _, t := range d.Combat.AttackTypes {
if combat.IsMeleeType(t) {
return t
}
}
return d.Combat.AttackTypes[0]
}
// SpeedTicks returns the hazard's tick interval (default 5).
func (d *HazardDef) SpeedTicks() float64 {
if d.Combat == nil || d.Combat.Stats.Speed <= 0 {
return 5
}
return d.Combat.Stats.Speed
}
// EffectiveMaxScienceHit applies the hazard's science percent bonus to its max
// science hit.
func (d *HazardDef) EffectiveMaxScienceHit() int {
if d.Combat == nil {
return 0
}
base := d.Combat.Stats.MaxScienceHit
if d.Combat.Stats.Bonuses.SciencePercentBonus > 0 {
base = int(float64(base) * (1.0 + float64(d.Combat.Stats.Bonuses.SciencePercentBonus)/100.0))
}
if base < 1 {
base = 1
}
return base
}
// 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 = behavior.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)
}
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 = behavior.BuildPathIndex(filepath.Join(w.dataDir, "hazards"))
}
ids := make(map[string]bool, len(w.hazardPathIndex))
for id := range w.hazardPathIndex {
ids[id] = true
}
return ids
}
|