aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_attack.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/cmd_attack.go')
-rw-r--r--internal/game/cmd_attack.go471
1 files changed, 471 insertions, 0 deletions
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
new file mode 100644
index 0000000..ebf88ac
--- /dev/null
+++ b/internal/game/cmd_attack.go
@@ -0,0 +1,471 @@
+package game
+
+import (
+ "fmt"
+ "sort"
+ "strconv"
+ "strings"
+
+ "thirdcollapse/internal/combat"
+ "thirdcollapse/internal/net"
+ "thirdcollapse/internal/object"
+ "thirdcollapse/internal/player"
+ "thirdcollapse/internal/world"
+)
+
+func (g *Game) doAttack(sess *net.Session, input string) {
+ p := sess.Player.(*player.Player)
+
+ if combat.GetCombat(p.Name) != nil {
+ sess.WriteLine("You are already in combat!")
+ return
+ }
+
+ if p.Action != nil {
+ g.CancelAction(p)
+ }
+
+ mob := g.findMob(sess, input, p.RoomID)
+ if mob == nil {
+ return
+ }
+
+ if mob.HP <= 0 {
+ sess.WriteLine("That is already dead.")
+ return
+ }
+
+ if mob.Protected {
+ sess.WriteLine(fmt.Sprintf("You can't attack %s!", mobDisplayName(mob, true)))
+ return
+ }
+
+ if combat.IsMobInCombat(mob.InstanceID) {
+ sess.WriteLine(fmt.Sprintf("%s is already engaged in combat!", mobDisplayName(mob, false)))
+ return
+ }
+
+ g.startCombat(sess, p, mob)
+}
+
+func (g *Game) findMob(sess *net.Session, input string, roomID int) *world.MobInstance {
+ lower := strings.ToLower(input)
+ mobs := g.MobStore.MobsInRoom(roomID)
+
+ idx := -1
+ name := lower
+ if dotPos := strings.Index(lower, "."); dotPos > 0 {
+ if n, err := strconv.Atoi(lower[:dotPos]); err == nil && n > 0 {
+ idx = n
+ name = lower[dotPos+1:]
+ }
+ }
+
+ var exact []*world.MobInstance
+ var prefix []*world.MobInstance
+ for _, m := range mobs {
+ q := m.MatchQuality(name)
+ if q == world.MatchExact {
+ exact = append(exact, m)
+ } else if q == world.MatchPrefix {
+ prefix = append(prefix, m)
+ }
+ }
+
+ candidates := exact
+ if len(candidates) == 0 {
+ candidates = prefix
+ }
+
+ if len(candidates) == 0 {
+ sess.WriteLine(fmt.Sprintf("There's no '%s' here.", input))
+ return nil
+ }
+
+ sort.Slice(candidates, func(i, j int) bool {
+ return candidates[i].InstanceID < candidates[j].InstanceID
+ })
+
+ if idx > 0 {
+ if idx-1 < len(candidates) {
+ return candidates[idx-1]
+ }
+ return nil
+ }
+
+ if len(candidates) == 1 {
+ return candidates[0]
+ }
+
+ seen := make(map[string]bool)
+ for _, m := range candidates {
+ seen[mobDisplayName(m, false)] = true
+ }
+ if len(seen) > 1 {
+ sess.WriteLine("Which one?")
+ return nil
+ }
+ return candidates[0]
+}
+
+func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ g.cancelRest(p.Name)
+
+ combat.EnterCombat(p.Name, mob.InstanceID)
+
+ playerSpeed := g.playerWeaponSpeed(p)
+
+ attBonus, strBonus, defBonus := combat.AttackStyleBonus(string(p.AttackStyle))
+ var styleParts []string
+ if attBonus > 0 {
+ styleParts = append(styleParts, fmt.Sprintf("+%d atk", attBonus))
+ }
+ if strBonus > 0 {
+ styleParts = append(styleParts, fmt.Sprintf("+%d str", strBonus))
+ }
+ if defBonus > 0 {
+ styleParts = append(styleParts, fmt.Sprintf("+%d def", defBonus))
+ }
+ styleStr := ""
+ if len(styleParts) > 0 {
+ styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")"
+ }
+ sess.WriteLine(fmt.Sprintf("\nYou attack %s!%s", mobDisplayName(mob, true), styleStr))
+
+ g.Ticks.Subscribe(playerSpeed, func() bool {
+ cs := combat.GetCombat(p.Name)
+ if cs == nil || !cs.Active {
+ return false
+ }
+ currentMob := g.MobStore.GetInstance(cs.MobID)
+ if currentMob == nil || currentMob.HP <= 0 {
+ g.endCombat(sess, p, currentMob)
+ return false
+ }
+ g.playerAttack(sess, p, currentMob)
+ if currentMob.HP <= 0 {
+ g.endCombat(sess, p, currentMob)
+ return false
+ }
+ return true
+ })
+
+ g.Ticks.Subscribe(mob.Speed, func() bool {
+ cs := combat.GetCombat(p.Name)
+ if cs == nil || !cs.Active {
+ return false
+ }
+ currentMob := g.MobStore.GetInstance(cs.MobID)
+ if currentMob == nil || currentMob.HP <= 0 {
+ return false
+ }
+ g.mobAttack(sess, p, currentMob)
+ if p.HP <= 0 {
+ g.endCombat(sess, p, currentMob)
+ return false
+ }
+ return true
+ })
+}
+
+func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle))
+
+ equipAtt := 0
+ equipStr := 0
+ if itemID, ok := p.Equipment[object.SlotMainHand]; ok {
+ def, err := g.ItemStore.Load(itemID)
+ if err == nil {
+ equipAtt = def.Stats.AttackBonus
+ equipStr = def.Stats.StrengthBonus
+ }
+ }
+
+ attRoll := combat.AttackRoll(p.Level(player.Attack), attBonus, equipAtt)
+ defRoll := combat.DefenseRoll(mob.Defense, 0, 0)
+
+ if combat.HitCheck(attRoll, defRoll) {
+ maxHit := combat.MaxHit(p.Level(player.Strength), strBonus, equipStr)
+ dmg := combat.RollDamage(maxHit)
+
+ mob.HP -= dmg
+ if mob.HP < 0 {
+ mob.HP = 0
+ }
+ if mob.HP < mob.MaxHP && mob.HP > 0 {
+ mob.StartRegen()
+ }
+ gains := g.awardCombatXP(p, dmg)
+
+ mobName := mobDisplayName(mob, true)
+ attacker := mob.Name
+ if !mob.Unique {
+ attacker = "The " + mob.Name
+ }
+ w := len(fmt.Sprintf(" You hit %s for %d damage.", mobName, dmg))
+ if w2 := len(fmt.Sprintf(" %s hits you for %d damage.", attacker, dmg)); w2 > w {
+ w = w2
+ }
+ g.combatPadWidth = w
+ prefix := fmt.Sprintf(" You hit %s for %d damage.", mobName, dmg)
+ hpPart := fmt.Sprintf("[%d/%dhp]", mob.HP, mob.MaxHP)
+ line := fmt.Sprintf("%-*s %s", g.combatPadWidth, prefix, hpPart)
+ if p.Toggles["xpdrops"] && len(gains) > 0 {
+ var parts []string
+ for _, gain := range gains {
+ parts = append(parts, fmt.Sprintf("+%dxp %s", gain.XP, player.SkillAbbr[player.SkillName(gain.Skill)]))
+ }
+ line += " (" + strings.Join(parts, ", ") + ")"
+ }
+ sess.WriteLine(line)
+ } else {
+ sess.WriteLine(fmt.Sprintf(" You miss %s.", mobDisplayName(mob, true)))
+ }
+}
+
+func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ _, _, defBonus := combat.AttackStyleBonus(string(p.AttackStyle))
+
+ equipDef := 0
+ for _, itemID := range p.Equipment {
+ def, err := g.ItemStore.Load(itemID)
+ if err == nil {
+ equipDef += def.Stats.DefenseBonus
+ }
+ }
+
+ attRoll := combat.AttackRoll(mob.Attack, 0, 0)
+ defRoll := combat.DefenseRoll(p.Level(player.Defense), defBonus, equipDef)
+
+ if combat.HitCheck(attRoll, defRoll) {
+ maxHit := combat.MaxHit(mob.Strength, 0, 0)
+ dmg := combat.RollDamage(maxHit)
+
+ 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, dmg))
+ if w2 := len(fmt.Sprintf(" You hit %s for %d damage.", mobName, dmg)); w2 > w {
+ w = w2
+ }
+ g.combatPadWidth = w
+ prefix := fmt.Sprintf(" %s hits you for %d damage.", attacker, dmg)
+ hpPart := fmt.Sprintf("[%d/%dhp]", p.HP, p.MaxHP())
+ sess.WriteLine(fmt.Sprintf("%-*s %s", g.combatPadWidth, prefix, hpPart))
+ } else {
+ attacker := mob.Name
+ if !mob.Unique {
+ attacker = "The " + mob.Name
+ }
+ sess.WriteLine(fmt.Sprintf(" %s misses you.", attacker))
+ }
+}
+
+func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ g.combatPadWidth = 0
+ combat.LeaveCombat(p.Name)
+
+ if p.HP <= 0 {
+ sess.WriteLine("\nOh dear, you are dead!")
+ g.dropItemsOnDeath(p)
+ p.HP = p.MaxHP()
+ p.RoomID = 1
+ g.AccountStore.SaveCharacter(p)
+ if g.Hub != nil {
+ g.Hub.EnterRoom(sess, p.RoomID)
+ }
+ g.doLook(sess)
+ return
+ }
+
+ if mob != nil && mob.HP <= 0 {
+ sess.WriteLine(fmt.Sprintf("\nYou have defeated %s!", mobDisplayName(mob, true)))
+ if g.Hub != nil {
+ for _, other := range g.Hub.PlayersInRoom(p.RoomID) {
+ if other != sess && other.Player != nil {
+ other.WriteLine(fmt.Sprintf("\n%s has slain %s (level %d)!", p.Name, mobDisplayName(mob, false), mobCombatLevel(mob)))
+ }
+ }
+ }
+
+ if mob.Drops.Remains != "" {
+ g.World.AddReservedGroundItem(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
+ }
+ dropper := mob.Name
+ if !mob.Unique {
+ dropper = "The " + mob.Name
+ }
+ sess.WriteLine(fmt.Sprintf(" %s drops: %s", dropper, name))
+ }
+
+ if len(mob.Drops.Loot) > 0 {
+ entry := g.BehaviorStore.ResolveDrop(mob.Drops.Loot)
+ if entry != nil && entry.ItemID != "" {
+ qty := entry.Quantity
+ if qty <= 0 {
+ qty = 1
+ }
+ g.World.AddReservedGroundItem(p.RoomID, entry.ItemID, qty, p.Name)
+ def, _ := g.ItemStore.Load(entry.ItemID)
+ name := entry.ItemID
+ if def != nil {
+ name = def.Name
+ }
+ dropper := mob.Name
+ if !mob.Unique {
+ dropper = "The " + mob.Name
+ }
+ if qty > 1 {
+ sess.WriteLine(fmt.Sprintf(" %s drops: %d x %s", dropper, qty, name))
+ } else {
+ sess.WriteLine(fmt.Sprintf(" %s drops: %s", dropper, name))
+ }
+ }
+ }
+
+ respawnTicks := mob.RespawnTicks
+ if respawnTicks <= 0 {
+ respawnTicks = 30
+ }
+ instanceID := mob.InstanceID
+ g.Ticks.Subscribe(respawnTicks, func() bool {
+ g.respawnMob(instanceID)
+ return false
+ })
+ }
+}
+
+func (g *Game) awardCombatXP(p *player.Player, dmg int) []xpGain {
+ baseXP := dmg * 4
+ var gains []xpGain
+
+ switch p.AttackStyle {
+ case player.Accurate:
+ gains = []xpGain{{string(player.Attack), 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.Attack), quarter},
+ {string(player.Strength), quarter},
+ {string(player.Defense), quarter},
+ {string(player.Hitpoints), quarter},
+ }
+ }
+
+ for _, gain := range gains {
+ p.AddXP(player.SkillName(gain.Skill), gain.XP)
+ }
+ g.AccountStore.SaveCharacter(p)
+ return gains
+}
+
+func (g *Game) dropItemsOnDeath(p *player.Player) {
+ roomID := p.RoomID
+
+ if p.Credits > 0 {
+ g.World.AddGroundItem(roomID, "credits", p.Credits)
+ p.Credits = 0
+ }
+
+ var items []deathDrop
+
+ for slot, inv := range p.Inventory {
+ if inv == nil || inv.Quantity <= 0 {
+ continue
+ }
+ val := 0
+ if def, err := g.ItemStore.Load(inv.ItemID); err == nil {
+ val = def.Value * inv.Quantity
+ }
+ items = append(items, deathDrop{
+ itemID: inv.ItemID,
+ quantity: inv.Quantity,
+ totalVal: val,
+ invSlot: slot,
+ })
+ }
+
+ for eqSlot, itemID := range p.Equipment {
+ val := 0
+ if def, err := g.ItemStore.Load(itemID); err == nil {
+ val = def.Value
+ }
+ items = append(items, deathDrop{
+ itemID: itemID,
+ quantity: 1,
+ totalVal: val,
+ isEquip: true,
+ equipSlot: eqSlot,
+ })
+ }
+
+ if len(items) <= 3 {
+ return
+ }
+
+ sort.Slice(items, func(i, j int) bool {
+ return items[i].totalVal > items[j].totalVal
+ })
+
+ for i := 3; i < len(items); i++ {
+ it := items[i]
+ if it.isEquip {
+ delete(p.Equipment, it.equipSlot)
+ } else {
+ p.SetInvSlot(it.invSlot, nil)
+ }
+ g.World.AddGroundItem(roomID, it.itemID, it.quantity)
+ }
+}
+
+func (g *Game) stopCombat(playerName string) {
+ if cs := combat.GetCombat(playerName); cs != nil {
+ combat.LeaveCombat(playerName)
+ }
+}
+
+func (g *Game) respawnMob(instanceID string) {
+ inst := g.MobStore.GetInstance(instanceID)
+ if inst == nil {
+ return
+ }
+ homeRoom := inst.HomeRoomID
+ inst.RoomID = homeRoom
+ inst.HP = inst.MaxHP
+ g.MobStore.RollIdleDescription(inst)
+
+ if g.Hub != nil {
+ for _, sess := range g.Hub.PlayersInRoom(homeRoom) {
+ if p, ok := sess.Player.(*player.Player); ok && p.Toggles["mobspawn"] {
+ sess.WriteLine(fmt.Sprintf("\n%s (level %d) enters the area.", mobDisplayName(inst, false), mobCombatLevel(inst)))
+ }
+ }
+ }
+}
+
+func (g *Game) playerWeaponSpeed(p *player.Player) int {
+ if itemID, ok := p.Equipment[object.SlotMainHand]; ok {
+ def, err := g.ItemStore.Load(itemID)
+ if err == nil && def.Speed > 0 {
+ return def.Speed
+ }
+ }
+ return 5
+}