aboutsummaryrefslogtreecommitdiff
path: root/internal/item
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-25 15:40:48 -0400
committerhistoria <[not public]>2026-06-25 15:40:48 -0400
commitabd612c15799f604e671e83dc7c410ed2b44185f (patch)
tree0597ca92350de73aac2a7cf26b2f2d6595dac0cc /internal/item
parent2725e2927a1595c7b100d942d1f14146252adeb7 (diff)
downloadthehouseoficarus-abd612c15799f604e671e83dc7c410ed2b44185f.tar.gz
slop refactor
Diffstat (limited to 'internal/item')
-rw-r--r--internal/item/doc.go4
-rw-r--r--internal/item/item.go191
-rw-r--r--internal/item/store.go78
3 files changed, 273 insertions, 0 deletions
diff --git a/internal/item/doc.go b/internal/item/doc.go
new file mode 100644
index 0000000..8fef8a7
--- /dev/null
+++ b/internal/item/doc.go
@@ -0,0 +1,4 @@
+// Package item defines item definitions (ItemDef), equipment stats, weapon
+// types, and the ItemStore YAML loader. Items are read from data/items/ on
+// demand and cached; editing a YAML takes effect immediately.
+package item
diff --git a/internal/item/item.go b/internal/item/item.go
new file mode 100644
index 0000000..890fa8d
--- /dev/null
+++ b/internal/item/item.go
@@ -0,0 +1,191 @@
+package item
+
+import (
+ "fmt"
+ "strings"
+
+ "gopkg.in/yaml.v3"
+ "thehouseoficarus/internal/behavior"
+)
+
+type EquipSlot string
+
+const (
+ SlotHead EquipSlot = "head"
+ SlotNeck EquipSlot = "neck"
+ SlotTorso EquipSlot = "torso"
+ SlotLegs EquipSlot = "legs"
+ SlotHands EquipSlot = "hands"
+ SlotFeet EquipSlot = "feet"
+ SlotBack EquipSlot = "back"
+ SlotAmmo EquipSlot = "ammo"
+ SlotMainHand EquipSlot = "main_hand"
+ SlotOffHand EquipSlot = "off_hand"
+ SlotRing EquipSlot = "ring"
+)
+
+type WeaponType string
+
+const (
+ WeaponMelee WeaponType = "melee"
+ WeaponRanged WeaponType = "ranged"
+ WeaponScience WeaponType = "science"
+)
+
+type ItemDef struct {
+ ID string `yaml:"id"`
+ Name string `yaml:"name"`
+ Color string `yaml:"color"`
+ Description string `yaml:"description"`
+ Value int `yaml:"value"`
+ Stackable bool `yaml:"stackable"`
+ EquipSlot EquipSlot `yaml:"equip_slot"`
+ WeaponType WeaponType `yaml:"weapon_type"`
+ AttackType string `yaml:"attack_type"`
+ Stats ItemStats `yaml:"stats"`
+ Speed float64 `yaml:"speed"`
+ Requirements map[string]int `yaml:"requirements,omitempty"`
+ ToolType string `yaml:"tool_type"`
+ ToolSpeed float64 `yaml:"tool_speed"`
+ BurnTicks float64 `yaml:"burn_ticks"`
+ FireLevel int `yaml:"fire_level"`
+ FireXP int `yaml:"fire_xp"`
+ Quality int `yaml:"quality"`
+ MaxQuality int `yaml:"max_quality"`
+ SearchTable string `yaml:"search_table"`
+ SearchMiscTable string `yaml:"search_misc_table"`
+ SearchTicks float64 `yaml:"search_ticks"`
+ SearchMessage string `yaml:"search_message"`
+ HealValue int `yaml:"heal_value"`
+ EatMessage string `yaml:"eat_message"`
+ Craft CraftList `yaml:"craft,omitempty"`
+ ProvidesJunk string `yaml:"provides_junk,omitempty"`
+ FarmPatchType string `yaml:"farm_patch_type"`
+ FarmLevel int `yaml:"farm_level"`
+ FarmPlantXP int `yaml:"farm_plant_xp"`
+ FarmHarvestXP int `yaml:"farm_harvest_xp"`
+ FarmStages int `yaml:"farm_stages"`
+ FarmProduct string `yaml:"farm_product"`
+ FarmMinYield int `yaml:"farm_min_yield"`
+ FarmMaxYield int `yaml:"farm_max_yield"`
+ PotionEffect string `yaml:"potion_effect"`
+ PotionBonus int `yaml:"potion_bonus"`
+ PotionDuration float64 `yaml:"potion_duration"`
+ Recoverable bool `yaml:"recoverable"`
+}
+
+type CraftList []CraftDef
+
+func (cl *CraftList) UnmarshalYAML(value *yaml.Node) error {
+ switch value.Kind {
+ case yaml.SequenceNode:
+ return value.Decode((*[]CraftDef)(cl))
+ case yaml.MappingNode:
+ var single CraftDef
+ if err := value.Decode(&single); err != nil {
+ return err
+ }
+ *cl = []CraftDef{single}
+ return nil
+ }
+ return fmt.Errorf("craft: expected mapping or sequence")
+}
+
+func (d *ItemDef) FirstCraft() *CraftDef {
+ if len(d.Craft) == 0 {
+ return nil
+ }
+ return &d.Craft[0]
+}
+
+func (d *ItemDef) FindCraft(fn func(CraftDef) bool) *CraftDef {
+ for i := range d.Craft {
+ if fn(d.Craft[i]) {
+ return &d.Craft[i]
+ }
+ }
+ return nil
+}
+
+func (d *ItemDef) MatchingCrafts(fn func(CraftDef) bool) []*CraftDef {
+ var result []*CraftDef
+ for i := range d.Craft {
+ if fn(d.Craft[i]) {
+ result = append(result, &d.Craft[i])
+ }
+ }
+ return result
+}
+
+type ConsumeEntry struct {
+ Items []string `yaml:"items"`
+ Quantity int `yaml:"quantity"`
+ Byproducts []string `yaml:"byproducts"`
+}
+
+type CraftStep struct {
+ Tick float64 `yaml:"tick"`
+ Message string `yaml:"message"`
+}
+
+type SuccessFormula struct {
+ Base float64 `yaml:"base"`
+ PerLevel float64 `yaml:"per_level"`
+ Cap float64 `yaml:"cap"`
+}
+
+type CraftDef struct {
+ Type string `yaml:"type"`
+ Skill string `yaml:"skill"`
+ Level int `yaml:"level"`
+ XP int `yaml:"xp"`
+ Wait float64 `yaml:"wait"`
+ Station []string `yaml:"station"`
+ Tool string `yaml:"tool"`
+ Consume []ConsumeEntry `yaml:"consume"`
+ OutputQty int `yaml:"output_qty"`
+ Fail string `yaml:"fail"`
+ Message string `yaml:"message"`
+ FailMessage string `yaml:"fail_message"`
+ StartMessage string `yaml:"start_message"`
+ EndMessage string `yaml:"end_message"`
+ Steps []CraftStep `yaml:"steps"`
+ Success *SuccessFormula `yaml:"success"`
+}
+
+func (c *CraftDef) EffectiveSkill() string {
+ if c.Skill != "" {
+ return c.Skill
+ }
+ return c.Type
+}
+
+type ItemStats struct {
+ StabAttack int `yaml:"stab_attack"`
+ SlashAttack int `yaml:"slash_attack"`
+ CrushAttack int `yaml:"crush_attack"`
+ ScienceAttack int `yaml:"science_attack"`
+ RangedAttack int `yaml:"ranged_attack"`
+
+ StabDefense int `yaml:"stab_defense"`
+ SlashDefense int `yaml:"slash_defense"`
+ CrushDefense int `yaml:"crush_defense"`
+ ScienceDefense int `yaml:"science_defense"`
+ RangedDefense int `yaml:"ranged_defense"`
+
+ StrengthBonus int `yaml:"strength_bonus"`
+ RangedStrength int `yaml:"ranged_strength"`
+ ScienceDamage int `yaml:"science_damage"`
+ TechnologyBonus int `yaml:"technology_bonus"`
+}
+
+func (d *ItemDef) MatchesName(input string) bool {
+ lower := strings.ToLower(strings.TrimSpace(input))
+ if lower == "" {
+ return false
+ }
+ if strings.ToLower(d.Name) == lower {
+ return true
+ }
+ return behavior.WordPrefixMatch(lower, d.Name)
+}
diff --git a/internal/item/store.go b/internal/item/store.go
new file mode 100644
index 0000000..d73d6fe
--- /dev/null
+++ b/internal/item/store.go
@@ -0,0 +1,78 @@
+package item
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "gopkg.in/yaml.v3"
+ "thehouseoficarus/internal/behavior"
+)
+
+type ItemStore struct {
+ dataDir string
+ pathIndex map[string]string
+ cache map[string]*ItemDef
+}
+
+func NewItemStore(dataDir string) *ItemStore {
+ s := &ItemStore{
+ dataDir: dataDir,
+ pathIndex: behavior.BuildPathIndex(filepath.Join(dataDir, "items")),
+ cache: make(map[string]*ItemDef),
+ }
+ return s
+}
+
+func (s *ItemStore) Load(id string) (*ItemDef, error) {
+ if def, ok := s.cache[id]; ok {
+ return def, nil
+ }
+ path, ok := s.pathIndex[id]
+ if !ok {
+ return nil, fmt.Errorf("read item %s: no such item", id)
+ }
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("read item %s: %w", id, err)
+ }
+ var def ItemDef
+ if err := yaml.Unmarshal(data, &def); err != nil {
+ return nil, fmt.Errorf("parse item %s: %w", id, err)
+ }
+ def.ID = id
+ s.cache[id] = &def
+ return &def, nil
+}
+
+func (s *ItemStore) LoadAll() ([]*ItemDef, error) {
+ dir := filepath.Join(s.dataDir, "items")
+ var defs []*ItemDef
+ err := behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error {
+ if def, ok := s.cache[id]; ok {
+ defs = append(defs, def)
+ return nil
+ }
+ var def ItemDef
+ if err := yaml.Unmarshal(data, &def); err != nil {
+ return fmt.Errorf("parse item %s: %w", id, err)
+ }
+ def.ID = id
+ s.cache[id] = &def
+ defs = append(defs, &def)
+ return nil
+ })
+ return defs, err
+}
+
+func (s *ItemStore) PathIndex() map[string]string {
+ return s.pathIndex
+}
+
+func (s *ItemStore) IDSet() map[string]bool {
+ ids := make(map[string]bool)
+ for id := range s.pathIndex {
+ ids[id] = true
+ }
+ return ids
+}