aboutsummaryrefslogtreecommitdiff
path: root/internal/game/combat_mob.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-25 15:40:48 -0400
committerhistoria <[not public]>2026-06-25 15:40:48 -0400
commitabd612c15799f604e671e83dc7c410ed2b44185f (patch)
tree0597ca92350de73aac2a7cf26b2f2d6595dac0cc /internal/game/combat_mob.go
parent2725e2927a1595c7b100d942d1f14146252adeb7 (diff)
downloadthehouseoficarus-abd612c15799f604e671e83dc7c410ed2b44185f.tar.gz
slop refactor
Diffstat (limited to 'internal/game/combat_mob.go')
-rw-r--r--internal/game/combat_mob.go301
1 files changed, 301 insertions, 0 deletions
diff --git a/internal/game/combat_mob.go b/internal/game/combat_mob.go
new file mode 100644
index 0000000..e06e307
--- /dev/null
+++ b/internal/game/combat_mob.go
@@ -0,0 +1,301 @@
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/color"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ if g.isSafespotted(p.Name) && g.safespotBlocksMob(p, mob) {
+ return
+ }
+
+ attRoll, defRoll := g.calculateMobAttack(p, mob)
+
+ if combat.HitCheck(attRoll, defRoll) {
+ g.applyMobHit(sess, p, mob)
+ } else {
+ g.applyMobMiss(sess, mob)
+ }
+}
+
+func (g *Game) calculateMobAttack(p *player.Player, mob *world.MobInstance) (attRoll int, defRoll int) {
+ _, _, defStyleBonus := combat.AttackStyleBonus(string(p.AttackStyle))
+
+ mobAttackType := mob.AttackType
+ if mobAttackType == "" {
+ mobAttackType = "crush"
+ }
+
+ attRoll = combat.EffectiveRoll(mob.Attack, 0, mob.AttackBonus)
+
+ totals := g.playerEquipBonuses(p)
+ equipDef := combat.SelectBonus(mobAttackType,
+ 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)
+ return
+}
+
+func (g *Game) applyMobHit(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ maxHit := combat.MaxHit(mob.Strength, 0, mob.StrengthBonus)
+ dmg := combat.RollDamage(maxHit)
+ dmg = g.applyTechProtection(p, mob, dmg)
+
+ if mob.DamageWithout != "" {
+ hasProtection := false
+ for _, itemID := range p.Equipment {
+ if itemID == mob.DamageWithout {
+ hasProtection = true
+ break
+ }
+ }
+ if !hasProtection {
+ dmg = dmg * 3 / 2
+ if dmg < 1 {
+ dmg = 1
+ }
+ cs := g.Combat.Get(p.Name)
+ if cs != nil && !cs.DamageWarningShown {
+ cs.DamageWarningShown = true
+ fbDef, _ := g.ItemStore.Load(mob.DamageWithout)
+ fbName := mob.DamageWithout
+ if fbDef != nil {
+ fbName = fbDef.Name
+ }
+ attacker := mob.Name
+ if !mob.Unique {
+ attacker = "The " + mob.Name
+ }
+ sess.WriteLine(g.colorize(sess, "warning",
+ fmt.Sprintf(" %s's attack is extra effective! Equip %s for protection.",
+ attacker, fbName)))
+ }
+ }
+ }
+
+ p.HP -= dmg
+ if p.HP < 0 {
+ p.HP = 0
+ }
+ p.StartRegen()
+ g.AccountStore.SaveCharacter(p)
+
+ attacker := mob.Name
+ if !mob.Unique {
+ attacker = "The " + mob.Name
+ }
+ mobName := mobDisplayName(mob, true)
+ w := len(fmt.Sprintf("%s hits you for %d damage.", attacker, 999))
+ if w2 := len(fmt.Sprintf("You hit %s for %d damage.", mobName, 999)); w2 > w {
+ w = w2
+ }
+ prefix := fmt.Sprintf("%s hits you for %s damage.", g.colorize(sess, "mob", attacker), g.colorize(sess, "damage_taken", fmt.Sprint(dmg)))
+ visLen := color.VisibleLen(prefix)
+ if visLen < w {
+ prefix += strings.Repeat(" ", w-visLen+1)
+ }
+ hpSuffix := fmt.Sprintf("%s [%2d/%d]", g.hpBar(sess, p.HP, p.MaxHP()), p.HP, p.MaxHP())
+ sess.WriteLine(prefix + hpSuffix)
+
+ if ss, hasSS := g.safespot.Get(p.Name); hasSS && ss.HideCountdown > 0 {
+ g.safespot.Delete(p.Name)
+ sess.WriteLine(g.colorize(sess, "error", "You're hit while repositioning! You failed to hide."))
+ }
+
+ if p.MoveTicks > 0 {
+ p.ClearMoveState()
+ sess.WriteLine("Can't escape!")
+ }
+}
+
+func (g *Game) applyMobMiss(sess *net.Session, mob *world.MobInstance) {
+ attacker := mob.Name
+ if !mob.Unique {
+ attacker = "The " + mob.Name
+ }
+ sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("%s misses you.", attacker)))
+}
+
+func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ g.Combat.Leave(p.Name)
+
+ if p.HP <= 0 {
+ g.killPlayer(sess, p, mob)
+ return
+ }
+
+ if mob != nil && mob.HP <= 0 {
+ isTask := mob.IsTask()
+ p.Stats.RecordMobKill(mob.DefID)
+
+ if isTask {
+ complete := mob.CompleteMessage
+ if complete == "" {
+ complete = fmt.Sprintf("You finish your work on %s!", mobDisplayName(mob, true))
+ }
+ sess.WriteLine(g.colorize(sess, "victory", "\n"+complete))
+ } else {
+ sess.WriteLine(g.colorize(sess, "victory", fmt.Sprintf("\nYou have defeated %s!", mobDisplayName(mob, true))))
+ g.onAssassinKill(sess, p, mob)
+ }
+
+ if g.Hub != nil {
+ for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
+ if other != sess && other.Player != nil {
+ if isTask {
+ other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s finishes working on %s.", p.Name, mobDisplayName(mob, false))))
+ } else {
+ op := other.Player
+ mobLvl := mobCombatLevel(mob)
+ levelStr := g.levelColorize(other, op.CombatLevel(), mobLvl, fmt.Sprintf("(level %d)", mobLvl))
+ other.WriteLine(g.colorize(other, "broadcast", fmt.Sprintf("\n%s has slain %s %s!", p.Name, mobDisplayName(mob, false), levelStr)))
+ }
+ }
+ }
+ }
+
+ dropLabel := func() string {
+ if isTask {
+ return "You receive:"
+ }
+ dropper := mob.Name
+ if !mob.Unique {
+ dropper = "The " + mob.Name
+ }
+ return dropper + " drops:"
+ }()
+
+ if mob.Drops.Remains != "" {
+ g.World.AddReservedItem(p.RoomID, mob.Drops.Remains, 1, p.Name)
+ def, _ := g.ItemStore.Load(mob.Drops.Remains)
+ name := mob.Drops.Remains
+ if def != nil {
+ name = def.Name
+ }
+ coloredName := g.itemColorize(sess, def, name)
+ sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropLabel), coloredName))
+ }
+
+ if len(mob.Drops.Loot) > 0 {
+ entry := behavior.ResolveDrop(g.DataDir, mob.Drops.Loot)
+ if entry != nil && entry.ItemID != "" {
+ qty := entry.Quantity
+ if qty <= 0 {
+ qty = 1
+ }
+ g.World.AddReservedItem(p.RoomID, entry.ItemID, qty, p.Name)
+ def, _ := g.ItemStore.Load(entry.ItemID)
+ name := entry.ItemID
+ if def != nil {
+ name = def.Name
+ }
+ coloredName := g.itemColorize(sess, def, name)
+ if qty > 1 {
+ sess.WriteLine(fmt.Sprintf("%s %d x %s", g.colorize(sess, "drop_message", dropLabel), qty, coloredName))
+ } else {
+ sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropLabel), coloredName))
+ }
+ }
+ }
+
+ respawnTicks := engine.ToTicks(mob.RespawnTicks)
+ if respawnTicks <= 0 {
+ respawnTicks = engine.ToTicks(30)
+ }
+ instanceID := mob.InstanceID
+ g.Ticks.Subscribe(respawnTicks, func() bool {
+ g.respawnMob(instanceID)
+ return false
+ })
+ g.writePrompt(sess)
+ }
+}
+
+// killPlayer handles a player death from any source (combat or a room hazard).
+// mob may be nil (e.g. a hazard kill); it is only used for Dead Man's Switch
+// retribution.
+func (g *Game) killPlayer(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ g.Combat.Leave(p.Name)
+
+ if p.HasActiveTech("retribution") {
+ retDef := GetTechDef("retribution")
+ if retDef != nil && mob != nil && mob.HP > 0 {
+ retDmg := int(float64(p.MaxHP()) * retDef.Effects.RetributionPct)
+ if retDmg > 0 {
+ mob.HP -= retDmg
+ if mob.HP < 0 {
+ mob.HP = 0
+ }
+ sess.WriteLine(fmt.Sprintf("\nDead Man's Switch activates! %s takes %d damage!",
+ mobDisplayName(mob, true), retDmg))
+ }
+ }
+ }
+ p.DeactivateAllTechs()
+ p.Stats.RecordDeath()
+ sess.WriteLine(g.colorize(sess, "death", "\nOh dear, you are dead!"))
+ p.ClearMoveState()
+ p.HazardTimer = 0
+ if ss, ok := g.safespot.Get(p.Name); ok {
+ g.forceLeaveSafespot(sess, p, &ss, "")
+ }
+ g.dropItemsOnDeath(p)
+ p.HP = p.MaxHP()
+ p.RoomID = 1001
+ g.AccountStore.SaveCharacter(p)
+ if g.Hub != nil {
+ g.Hub.EnterRoom(sess, p.RoomID)
+ }
+ g.doLook(sess)
+ g.writePrompt(sess)
+}
+
+func (g *Game) awardCombatXP(sess *net.Session, p *player.Player, dmg int, isRanged bool) []xpGain {
+ baseXP := dmg * 4
+ var gains []xpGain
+
+ if isRanged {
+ gains = []xpGain{
+ {string(player.Ranged), baseXP * 3 / 4},
+ {string(player.Hitpoints), baseXP / 4},
+ }
+ } else {
+ switch p.AttackStyle {
+ case player.Accurate:
+ gains = []xpGain{{string(player.Accuracy), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}}
+ case player.Aggressive:
+ gains = []xpGain{{string(player.Strength), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}}
+ case player.Defensive:
+ gains = []xpGain{{string(player.Defense), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}}
+ case player.Balanced:
+ quarter := baseXP / 4
+ gains = []xpGain{
+ {string(player.Accuracy), quarter},
+ {string(player.Strength), quarter},
+ {string(player.Defense), quarter},
+ {string(player.Hitpoints), quarter},
+ }
+ }
+ }
+
+ for _, gain := range gains {
+ g.awardSkillXP(sess, p, player.SkillName(gain.Skill), gain.XP)
+ }
+ g.AccountStore.SaveCharacter(p)
+ return gains
+}
+
+func (g *Game) stopCombat(playerName string) {
+ if cs := g.Combat.Get(playerName); cs != nil {
+ g.Combat.Leave(playerName)
+ }
+}