aboutsummaryrefslogtreecommitdiff
path: root/internal/game/sys_hazard.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/sys_hazard.go')
-rw-r--r--internal/game/sys_hazard.go136
1 files changed, 136 insertions, 0 deletions
diff --git a/internal/game/sys_hazard.go b/internal/game/sys_hazard.go
new file mode 100644
index 0000000..29bf65e
--- /dev/null
+++ b/internal/game/sys_hazard.go
@@ -0,0 +1,136 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+// HazardTick rolls environmental attacks against every player standing in a
+// hazardous room. It reuses the combat formulas exactly: the hazard "attacks"
+// and the player defends with their Defense level + equipment bonus selected by
+// the hazard's attack type. It runs independently of combat, so a player can be
+// hit by BOTH a mob and the room hazard in the same area. It deliberately never
+// touches movement state — a hazard hit does NOT interrupt walking.
+func (g *Game) HazardTick() {
+ if g.Hub == nil {
+ return
+ }
+ for _, sess := range g.Hub.AllSessions() {
+ p := sess.Player
+ if p == nil {
+ continue
+ }
+
+ room, err := g.World.LoadRoom(p.RoomID)
+ if err != nil || room.Hazard == "" {
+ p.HazardTimer = 0
+ continue
+ }
+ hz, err := g.World.LoadHazard(room.Hazard)
+ if err != nil || hz == nil {
+ p.HazardTimer = 0
+ continue
+ }
+
+ // A safespot shields the player from all hazard damage while hidden.
+ if g.safespotBlocksHazard(p) {
+ continue
+ }
+
+ // The right protective gear negates the hazard entirely.
+ if hz.RequiredItem != "" && g.hasEquipped(p, hz.RequiredItem) {
+ continue
+ }
+
+ speed := hz.Speed
+ if speed <= 0 {
+ speed = 5
+ }
+ p.HazardTimer++
+ if float64(p.HazardTimer) >= speed {
+ p.HazardTimer = 0
+ g.rollHazard(sess, p, hz)
+ }
+ }
+}
+
+func (g *Game) hasEquipped(p *player.Player, itemID string) bool {
+ for _, id := range p.Equipment {
+ if id == itemID {
+ return true
+ }
+ }
+ return false
+}
+
+func (g *Game) rollHazard(sess *net.Session, p *player.Player, hz *world.HazardDef) {
+ attackType := hz.AttackType
+ if attackType == "" {
+ attackType = "crush"
+ }
+
+ attRoll := combat.EffectiveRoll(hz.Attack, 0, hz.AttackBonus)
+
+ _, _, defStyleBonus := combat.AttackStyleBonus(string(p.AttackStyle))
+ totals := g.playerEquipBonuses(p)
+ equipDef := combat.SelectBonus(attackType,
+ totals.StabDefense, totals.SlashDefense, totals.CrushDefense,
+ totals.ScienceDefense, totals.RangedDefense)
+ defRoll := combat.EffectiveRoll(
+ p.Level(player.Defense)+g.techLevelBonus(p, "defense")+g.buffLevelBonus(p, "defense"),
+ defStyleBonus, equipDef)
+
+ if combat.HitCheck(attRoll, defRoll) {
+ g.applyHazardHit(sess, p, hz)
+ } else {
+ miss := hz.MissMessage
+ if miss == "" {
+ miss = fmt.Sprintf("You weather the %s.", hz.Name)
+ }
+ sess.WriteLine("\n" + g.colorize(sess, "miss", miss))
+ g.writePrompt(sess)
+ }
+}
+
+func (g *Game) applyHazardHit(sess *net.Session, p *player.Player, hz *world.HazardDef) {
+ maxHit := hz.MaxHit
+ if maxHit < 1 {
+ maxHit = 1
+ }
+ dmg := combat.RollDamage(maxHit)
+ dmg = g.damageAfterTechProtection(p, hz.AttackType, dmg)
+ if dmg < 0 {
+ dmg = 0
+ }
+
+ p.HP -= dmg
+ if p.HP < 0 {
+ p.HP = 0
+ }
+ p.StartRegen()
+ g.AccountStore.SaveCharacter(p)
+
+ msg := hz.HitMessage
+ switch {
+ case msg == "":
+ msg = fmt.Sprintf("The %s hits you for %d damage.", hz.Name, dmg)
+ case strings.Contains(msg, "%d"):
+ msg = fmt.Sprintf(msg, dmg)
+ }
+ hpSuffix := fmt.Sprintf(" %s [%d/%d]", g.hpBar(sess, p.HP, p.MaxHP()), p.HP, p.MaxHP())
+ sess.WriteLine("\n" + g.colorize(sess, "damage_taken", msg) + hpSuffix)
+
+ // NB: a hazard never clears MoveState, so it cannot interrupt a walk the way
+ // a mob hit ("Can't escape!") does.
+
+ if p.HP <= 0 {
+ g.killPlayer(sess, p, nil)
+ return
+ }
+ g.writePrompt(sess)
+}