aboutsummaryrefslogtreecommitdiff
path: root/internal/world/hazard.go
blob: c08c2bba656a67f703a6b824a92b7e4ef1f0f9c2 (plain)
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
}