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) } }