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)) p.ActionState = &ActionState{Type: ActionCombating, TargetName: mob.Name} g.Ticks.Subscribe(g.computeTicks(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(g.computeTicks(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, leveled := g.awardCombatXP(p, dmg) for _, skill := range leveled { sess.WriteLine(fmt.Sprintf("*** You are now level %d %s! ***", p.Level(skill), skill)) } 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.OptionBool("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) p.ActionState = nil 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 := g.computeTicks(mob.RespawnTicks) if respawnTicks <= 0 { respawnTicks = g.computeTicks(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, []player.SkillName) { baseXP := dmg * 4 var gains []xpGain var leveledUp []player.SkillName 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 { if newLevel := p.AddSkillXP(player.SkillName(gain.Skill), gain.XP); newLevel > 0 { leveledUp = append(leveledUp, player.SkillName(gain.Skill)) } } g.AccountStore.SaveCharacter(p) return gains, leveledUp } 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.OptionBool("mobspawn") { sess.WriteLine(fmt.Sprintf("\n%s (level %d) spawns in the area.", mobDisplayName(inst, false), mobCombatLevel(inst))) } } } } func (g *Game) playerWeaponSpeed(p *player.Player) float64 { 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.0 }