diff options
Diffstat (limited to 'internal/game/sys_hazard.go')
| -rw-r--r-- | internal/game/sys_hazard.go | 136 |
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) +} |
