aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-07 21:22:39 -0400
committerhistoria <[not public]>2026-07-07 21:22:39 -0400
commit74803657d587b1f93b739c1ee25bb8d62df95413 (patch)
tree0166974468c452bb13454d37d4b83d2e681446c0 /internal/game
parentbae42946d24bd533cdbc1b271ce14acd752689d0 (diff)
downloadthehouseoficarus-74803657d587b1f93b739c1ee25bb8d62df95413.tar.gz
fix: agility course creation fixed and improved in admin web gui
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/act_agility.go104
-rw-r--r--internal/game/core_course.go75
-rw-r--r--internal/game/core_course_test.go4
-rw-r--r--internal/game/game.go1
4 files changed, 138 insertions, 46 deletions
diff --git a/internal/game/act_agility.go b/internal/game/act_agility.go
index 18dbf4a..a243b44 100644
--- a/internal/game/act_agility.go
+++ b/internal/game/act_agility.go
@@ -44,6 +44,7 @@ func (g *Game) startObstacle(sess *net.Session, p *player.Player, info *Obstacle
TotalObstacles: info.TotalObstacles,
NextRoom: info.NextRoom,
StartRoom: info.StartRoom,
+ OnFailRoom: info.OnFailRoom,
ObstacleXP: info.ObstacleXP,
CompletionXP: info.CompletionXP,
FailChance: failPtr,
@@ -62,6 +63,11 @@ func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) {
phase := d.Phase
phases := d.Phases
+ if len(phases) == 0 {
+ g.completeObstacle(sess, p, d)
+ return
+ }
+
if phase >= len(phases) {
g.cancelAction(p)
return
@@ -84,58 +90,61 @@ func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) {
nextPhase := phase + 1
if nextPhase >= len(phases) {
- obstacleXP := d.ObstacleXP
- completionXP := d.CompletionXP
- nextRoom := d.NextRoom
- startRoom := d.StartRoom
- obstacleIndex := d.ObstacleIndex
- totalObstacles := d.TotalObstacles
- courseID := d.CourseID
-
- if obstacleXP > 0 {
- g.awardSkillXP(sess, p, player.Agility, obstacleXP)
- if p.OptionBool("xp_drops") {
- sess.WriteLine(g.colorize(sess, "xp",
- fmt.Sprintf("(+%dxp %s)", obstacleXP, player.SkillAbbr[player.Agility])))
- }
+ g.completeObstacle(sess, p, d)
+ return
+ }
+
+ d.Phase = nextPhase
+ p.Action.WaitLeft = engine.ToTicks(phases[nextPhase].Delay)
+}
+
+func (g *Game) completeObstacle(sess *net.Session, p *player.Player, d *behavior.ObstacleData) {
+ obstacleXP := d.ObstacleXP
+ completionXP := d.CompletionXP
+ nextRoom := d.NextRoom
+ obstacleIndex := d.ObstacleIndex
+ totalObstacles := d.TotalObstacles
+ courseID := d.CourseID
+
+ isLastObstacle := obstacleIndex == totalObstacles-1
+
+ if obstacleXP > 0 {
+ g.awardSkillXP(sess, p, player.Agility, obstacleXP)
+ if !isLastObstacle && p.OptionBool("xp_drops") {
+ sess.WriteLine(g.colorize(sess, "xp",
+ fmt.Sprintf("(+%dxp %s)", obstacleXP, player.SkillAbbr[player.Agility])))
}
+ }
- isLastObstacle := obstacleIndex == totalObstacles-1
-
- if isLastObstacle {
- if completionXP > 0 {
- g.awardSkillXP(sess, p, player.Agility, completionXP)
- if p.OptionBool("xp_drops") {
- sess.WriteLine(g.colorize(sess, "xp",
- fmt.Sprintf("(+%dxp %s course bonus)", completionXP, player.SkillAbbr[player.Agility])))
- }
- }
-
- lapKey := "agility_laps_" + courseID
- laps := getPlayerFlagInt(p, lapKey) + 1
- g.setPlayerFlag(p, lapKey, laps)
-
- courseName := p.Action.TargetName
- sess.WriteLine(g.colorize(sess, "broadcast",
- fmt.Sprintf("Course complete! %s lap %d finished.", courseName, laps)))
-
- g.cancelAction(p)
- g.teleportPlayer(sess, p, startRoom)
- } else {
- g.cancelAction(p)
- g.teleportPlayer(sess, p, nextRoom)
+ if isLastObstacle {
+ if completionXP > 0 {
+ g.awardSkillXP(sess, p, player.Agility, completionXP)
}
- g.AccountStore.SaveCharacter(p)
- return
+ lapKey := "agility_laps_" + courseID
+ laps := getPlayerFlagInt(p, lapKey) + 1
+ g.setPlayerFlag(p, lapKey, laps)
+
+ courseName := p.Action.TargetName
+ msg := fmt.Sprintf("You've completed %d lap%s of %s", laps, pluralLap(laps), courseName)
+ combinedXP := obstacleXP + completionXP
+ if p.OptionBool("xp_drops") && combinedXP > 0 {
+ msg += fmt.Sprintf(" (+%d%s)", combinedXP, player.SkillAbbr[player.Agility])
+ }
+ sess.WriteLine(g.colorize(sess, "broadcast", msg))
+
+ g.cancelAction(p)
+ g.teleportPlayer(sess, p, nextRoom)
+ } else {
+ g.cancelAction(p)
+ g.teleportPlayer(sess, p, nextRoom)
}
- d.Phase = nextPhase
- p.Action.WaitLeft = engine.ToTicks(phases[nextPhase].Delay)
+ g.AccountStore.SaveCharacter(p)
}
func (g *Game) obstacleFail(sess *net.Session, p *player.Player, d *behavior.ObstacleData) {
- startRoom := d.StartRoom
+ onFailRoom := d.OnFailRoom
failMin := d.FailDamageMin
failMax := d.FailDamageMax
@@ -155,7 +164,14 @@ func (g *Game) obstacleFail(sess *net.Session, p *player.Player, d *behavior.Obs
g.AccountStore.SaveCharacter(p)
g.cancelAction(p)
- g.teleportPlayer(sess, p, startRoom)
+ g.teleportPlayer(sess, p, onFailRoom)
+}
+
+func pluralLap(n int) string {
+ if n == 1 {
+ return ""
+ }
+ return "s"
}
// calcFailChance returns the failure probability for an obstacle attempt.
diff --git a/internal/game/core_course.go b/internal/game/core_course.go
index 54894fd..031eff5 100644
--- a/internal/game/core_course.go
+++ b/internal/game/core_course.go
@@ -1,11 +1,14 @@
package game
import (
+ "log"
+ "os"
"path/filepath"
"sync"
"gopkg.in/yaml.v3"
"thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/world"
)
// ObstaclePhase is a single phase of an obstacle's advancement sequence.
@@ -31,6 +34,8 @@ type ObstacleDef struct {
TicksPerPhase float64 `yaml:"ticks_per_phase"`
Messages []string `yaml:"messages"`
Phases []ObstaclePhase `yaml:"phases"`
+ ExitDir string `yaml:"exit_dir"`
+ OnFailRoom int `yaml:"on_fail,omitempty"`
}
type CourseConfig struct {
@@ -62,7 +67,9 @@ type ObstacleInfo struct {
FailDamage [2]int
NextRoom int
StartRoom int
+ OnFailRoom int
RequiredLevel int
+ ExitDir string
}
var obstacleVerbs = map[string]bool{}
@@ -85,6 +92,7 @@ type CourseStore struct {
courses map[string]*CourseConfig
roomToObstacle map[int]*ObstacleInfo
loaded bool
+ world *world.World
}
func NewCourseStore(dataDir string) *CourseStore {
@@ -94,6 +102,10 @@ func NewCourseStore(dataDir string) *CourseStore {
}
}
+func (cs *CourseStore) SetWorld(w *world.World) {
+ cs.world = w
+}
+
func (cs *CourseStore) LoadAll() {
cs.mu.Lock()
defer cs.mu.Unlock()
@@ -160,6 +172,52 @@ func resolvePhases(obs ObstacleDef) []PhaseInfo {
return phases
}
+func (cs *CourseStore) resolveExitTarget(roomID int, dir string) int {
+ if cs.world == nil || dir == "" {
+ return 0
+ }
+ path, ok := cs.world.GetRoomPath(roomID)
+ if !ok {
+ return 0
+ }
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return 0
+ }
+ var m map[string]any
+ if err := yaml.Unmarshal(data, &m); err != nil {
+ return 0
+ }
+ exits, _ := m["exits"].(map[string]any)
+ if exits == nil {
+ return 0
+ }
+ v, ok := exits[dir]
+ if !ok {
+ return 0
+ }
+ switch x := v.(type) {
+ case int:
+ return x
+ case int64:
+ return int(x)
+ case float64:
+ return int(x)
+ case map[string]any:
+ if r, ok := x["room"]; ok {
+ switch y := r.(type) {
+ case int:
+ return y
+ case int64:
+ return int(y)
+ case float64:
+ return int(y)
+ }
+ }
+ }
+ return 0
+}
+
func (cs *CourseStore) loadAllLocked() {
cs.loaded = true
cs.roomToObstacle = make(map[int]*ObstacleInfo)
@@ -178,6 +236,16 @@ func (cs *CourseStore) loadAllLocked() {
nextRoom := 0
if i < totalObstacles-1 {
nextRoom = cfg.Obstacles[i+1].RoomID
+ } else if obs.ExitDir != "" {
+ resolved := cs.resolveExitTarget(obs.RoomID, obs.ExitDir)
+ if resolved != 0 {
+ nextRoom = resolved
+ } else {
+ log.Printf("[WARNING] Course %q: last obstacle room %d has exit_dir=%q but no exit in that direction; falling back to start_room %d", cfg.ID, obs.RoomID, obs.ExitDir, cfg.StartRoom)
+ nextRoom = cfg.StartRoom
+ }
+ } else {
+ nextRoom = cfg.StartRoom
}
completionXP := 0
@@ -185,6 +253,11 @@ func (cs *CourseStore) loadAllLocked() {
completionXP = cfg.CompletionXP
}
+ onFailRoom := obs.OnFailRoom
+ if onFailRoom == 0 {
+ onFailRoom = cfg.StartRoom
+ }
+
info := &ObstacleInfo{
CourseID: cfg.ID,
CourseName: cfg.Name,
@@ -198,7 +271,9 @@ func (cs *CourseStore) loadAllLocked() {
FailDamage: obs.FailDamage,
NextRoom: nextRoom,
StartRoom: cfg.StartRoom,
+ OnFailRoom: onFailRoom,
RequiredLevel: cfg.RequiredLevel,
+ ExitDir: obs.ExitDir,
}
cs.roomToObstacle[obs.RoomID] = info
localVerbs[obs.Verb] = true
diff --git a/internal/game/core_course_test.go b/internal/game/core_course_test.go
index 3d9fdc2..64ec8ad 100644
--- a/internal/game/core_course_test.go
+++ b/internal/game/core_course_test.go
@@ -130,8 +130,8 @@ obstacles:
if o2.CompletionXP != 50 {
t.Errorf("last obstacle should carry completion_xp, got %d", o2.CompletionXP)
}
- if o2.NextRoom != 0 {
- t.Errorf("last obstacle should have next_room 0, got %d", o2.NextRoom)
+ if o2.NextRoom != 100 {
+ t.Errorf("last obstacle with no exit_dir should fall back to start_room 100, got %d", o2.NextRoom)
}
if cs.GetObstacle(999) != nil {
diff --git a/internal/game/game.go b/internal/game/game.go
index b9fb0af..f34e649 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -101,6 +101,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.Vali
hackingStates: make(map[string]*hacking.Session),
enterSeqs: make(map[string]*enterSeq),
}
+ g.CourseStore.SetWorld(g.World)
g.CourseStore.LoadAll()
g.LoadMods()
g.LoadTechs()