package game import ( "path/filepath" "sync" "gopkg.in/yaml.v3" "thehouseoficarus/internal/behavior" ) type ObstacleDef struct { RoomID int `yaml:"room_id"` Verb string `yaml:"verb"` TicksPerPhase float64 `yaml:"ticks_per_phase"` XP int `yaml:"xp"` FailDamage [2]int `yaml:"fail_damage"` Messages []string `yaml:"messages"` } type CourseConfig struct { ID string `yaml:"id"` Name string `yaml:"name"` RequiredLevel int `yaml:"required_level"` StartRoom int `yaml:"start_room"` CompletionXP int `yaml:"completion_xp"` Obstacles []ObstacleDef `yaml:"obstacles"` } type ObstacleInfo struct { CourseID string CourseName string ObstacleIndex int TotalObstacles int Verb string Messages []string TicksPerPhase float64 ObstacleXP int CompletionXP int FailDamage [2]int NextRoom int StartRoom int RequiredLevel int } var obstacleVerbs = map[string]bool{} var verbGerund = map[string]string{ "scramble": "scrambling", "jump": "jumping", "swing": "swinging", "balance": "balancing", "climb": "climbing", "crawl": "crawling", "vault": "vaulting", "leap": "leaping", "slide": "sliding", } type CourseStore struct { dataDir string mu sync.Mutex courses map[string]*CourseConfig roomToObstacle map[int]*ObstacleInfo loaded bool } func NewCourseStore(dataDir string) *CourseStore { return &CourseStore{ dataDir: dataDir, courses: make(map[string]*CourseConfig), } } func (cs *CourseStore) LoadAll() { cs.mu.Lock() defer cs.mu.Unlock() cs.loadAllLocked() } // AllCourses returns the full course config map, loading on first access. func (cs *CourseStore) AllCourses() map[string]*CourseConfig { cs.mu.Lock() defer cs.mu.Unlock() if !cs.loaded { cs.loadAllLocked() } return cs.courses } func (cs *CourseStore) GetObstacle(roomID int) *ObstacleInfo { cs.mu.Lock() defer cs.mu.Unlock() if !cs.loaded { cs.loadAllLocked() } return cs.roomToObstacle[roomID] } func (cs *CourseStore) loadAllLocked() { cs.loaded = true cs.roomToObstacle = make(map[int]*ObstacleInfo) localVerbs := make(map[string]bool) behavior.WalkYAMLDir(filepath.Join(cs.dataDir, "courses"), func(path, id string, data []byte) error { var cfg CourseConfig if err := yaml.Unmarshal(data, &cfg); err != nil { return nil } cfg.ID = id cs.courses[cfg.ID] = &cfg totalObstacles := len(cfg.Obstacles) for i, obs := range cfg.Obstacles { nextRoom := 0 if i < totalObstacles-1 { nextRoom = cfg.Obstacles[i+1].RoomID } completionXP := 0 if i == totalObstacles-1 { completionXP = cfg.CompletionXP } info := &ObstacleInfo{ CourseID: cfg.ID, CourseName: cfg.Name, ObstacleIndex: i, TotalObstacles: totalObstacles, Verb: obs.Verb, Messages: obs.Messages, TicksPerPhase: obs.TicksPerPhase, ObstacleXP: obs.XP, CompletionXP: completionXP, FailDamage: obs.FailDamage, NextRoom: nextRoom, StartRoom: cfg.StartRoom, RequiredLevel: cfg.RequiredLevel, } cs.roomToObstacle[obs.RoomID] = info localVerbs[obs.Verb] = true } return nil }) obstacleVerbs = localVerbs }