package game import ( "fmt" "math/rand" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) func (g *Game) startObstacle(sess *net.Session, p *player.Player, info *ObstacleInfo) { failChance := g.calcFailChance(p, info) var failPtr *float64 if failChance != nil { v := *failChance failPtr = &v } gerund := info.Verb + "ing" if gr, ok := verbGerund[info.Verb]; ok { gerund = gr } phases := make([]behavior.PhaseData, len(info.Phases)) for i, ph := range info.Phases { phases[i] = behavior.PhaseData{ Message: ph.Message, Delay: ph.Delay, FailCheck: ph.FailCheck, } } p.Action = &behavior.Action{ Type: behavior.TypeObstacle, TargetID: info.CourseID, TargetName: info.CourseName, WaitLeft: 0, Data: &behavior.ObstacleData{ CourseID: info.CourseID, Phase: 0, ObstacleIndex: info.ObstacleIndex, TotalObstacles: info.TotalObstacles, NextRoom: info.NextRoom, StartRoom: info.StartRoom, OnFailRoom: info.OnFailRoom, ObstacleXP: info.ObstacleXP, CompletionXP: info.CompletionXP, FailChance: failPtr, FailDamageMin: info.FailDamage[0], FailDamageMax: info.FailDamage[1], Phases: phases, RequiredLevel: info.RequiredLevel, }, } g.broadcastAction(sess, "%s starts %s on the %s.", p.Name, gerund, info.CourseName) } func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) { d := p.Action.Data.(*behavior.ObstacleData) phase := d.Phase phases := d.Phases if len(phases) == 0 { g.completeObstacle(sess, p, d) return } if phase >= len(phases) { g.cancelAction(p) return } ph := phases[phase] sess.WriteLine(g.colorize(sess, "broadcast", ph.Message)) if ph.FailCheck { failChance := 0.30 if d.FailChance != nil { failChance = *d.FailChance } if rand.Float64() < failChance { g.obstacleFail(sess, p, d) return } } nextPhase := phase + 1 if nextPhase >= len(phases) { 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]))) } } if isLastObstacle { if completionXP > 0 { g.awardSkillXP(sess, p, player.Agility, completionXP) } 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) } g.AccountStore.SaveCharacter(p) } func (g *Game) obstacleFail(sess *net.Session, p *player.Player, d *behavior.ObstacleData) { onFailRoom := d.OnFailRoom failMin := d.FailDamageMin failMax := d.FailDamageMax damage := failMin if failMax > failMin { damage = failMin + rand.Intn(failMax-failMin+1) } sess.WriteLine(g.colorize(sess, "damage_taken", "You slip and fall!")) p.HP -= damage if p.HP < 1 { p.HP = 1 } sess.WriteLine(g.colorize(sess, "damage_taken", fmt.Sprintf("You take %s damage. HP: %d/%d", g.dmgDisplay(sess, damage), p.HP, p.MaxHP()))) g.AccountStore.SaveCharacter(p) g.cancelAction(p) 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. // If the obstacle defines an explicit fail_chance it is used; otherwise the // chance is derived from the player's Agility level relative to the course's // required level (70% success at required, scaling to 95% above, capped). func (g *Game) calcFailChance(p *player.Player, info *ObstacleInfo) *float64 { if info.FailChance != nil { v := *info.FailChance if v < 0 { v = 0 } if v > 1 { v = 1 } return &v } level := p.Level(player.Agility) required := info.RequiredLevel chance := 0.30 - float64(level-required)*0.01 if chance < 0.05 { chance = 0.05 } if chance > 0.60 { chance = 0.60 } return &chance }