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.go275
1 files changed, 172 insertions, 103 deletions
diff --git a/internal/game/cmd_attack.go b/internal/game/cmd_attack.go
index 1f18d00..f3b99c0 100644
--- a/internal/game/cmd_attack.go
+++ b/internal/game/cmd_attack.go
@@ -35,7 +35,11 @@ func (g *Game) doAttack(sess *net.Session, input string) {
}
if mob.HP <= 0 {
- sess.WriteLine("That is already dead.")
+ if mob.IsTask() {
+ sess.WriteLine(fmt.Sprintf("%s is already complete.", mobDisplayName(mob, true)))
+ } else {
+ sess.WriteLine("That is already dead.")
+ }
return
}
@@ -133,6 +137,7 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
combat.EnterCombat(p.Name, mob.InstanceID)
p.AttackTimer = 0
+ isTask := mob.IsTask()
autotriggerActive := p.AutotriggerMod != ""
if !autotriggerActive {
@@ -151,20 +156,35 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
if len(styleParts) > 0 {
styleStr = " Style: " + string(p.AttackStyle) + " (" + strings.Join(styleParts, ", ") + ")"
}
- sess.WriteLine(fmt.Sprintf("You attack %s!%s", g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr))
+ if isTask {
+ sess.WriteLine(fmt.Sprintf("You begin to %s %s.%s", taskWorkVerb(mob), g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr))
+ } else {
+ sess.WriteLine(fmt.Sprintf("You attack %s!%s", g.colorize(sess, "mob", mobDisplayName(mob, true)), styleStr))
+ }
} else {
mod := GetMod(p.AutotriggerMod)
modName := p.AutotriggerMod
if mod != nil {
modName = mod.Name
}
- sess.WriteLine(fmt.Sprintf("You attack %s with %s!",
- g.colorize(sess, "mob", mobDisplayName(mob, true)),
- g.colorize(sess, "science_mod", modName)))
+ if isTask {
+ sess.WriteLine(fmt.Sprintf("You begin to %s %s using %s!",
+ taskWorkVerb(mob),
+ g.colorize(sess, "mob", mobDisplayName(mob, true)),
+ g.colorize(sess, "science_mod", modName)))
+ } else {
+ sess.WriteLine(fmt.Sprintf("You attack %s with %s!",
+ g.colorize(sess, "mob", mobDisplayName(mob, true)),
+ g.colorize(sess, "science_mod", modName)))
+ }
+ }
+ if isTask {
+ p.ActionState = &ActionState{Type: ActionWorking, TargetName: mob.Name}
+ g.broadcastAction(sess, "\n%s begins working on %s.", p.Name, mobDisplayName(mob, true))
+ } else {
+ p.ActionState = &ActionState{Type: ActionCombating, TargetName: mob.Name}
+ g.broadcastAction(sess, "\n%s attacks %s!", p.Name, mobDisplayName(mob, true))
}
- p.ActionState = &ActionState{Type: ActionCombating, TargetName: mob.Name}
-
- g.broadcastAction(sess, "\n%s attacks %s!", p.Name, mobDisplayName(mob, true))
g.Ticks.Subscribe(1, func() bool {
cs := combat.GetCombat(p.Name)
@@ -223,22 +243,24 @@ func (g *Game) startCombat(sess *net.Session, p *player.Player, mob *world.MobIn
return true
})
- g.Ticks.Subscribe(engine.ToTicks(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
- })
+ if !isTask {
+ g.Ticks.Subscribe(engine.ToTicks(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) {
@@ -299,8 +321,8 @@ func (g *Game) calculatePlayerAttackRoll(p *player.Player, mob *world.MobInstanc
equipAttack := combat.SelectBonus(attackType,
totals.StabAttack, totals.SlashAttack, totals.CrushAttack,
totals.ScienceAttack, totals.RangedAttack)
- effectiveAttack := p.Level(player.Attack) + g.techLevelBonus(p, "attack") + g.buffLevelBonus(p, "attack")
- attRoll = combat.EffectiveRoll(effectiveAttack, attBonus, equipAttack)
+ effectiveAccuracy := p.Level(player.Accuracy) + g.techLevelBonus(p, "accuracy") + g.buffLevelBonus(p, "accuracy")
+ attRoll = combat.EffectiveRoll(effectiveAccuracy, attBonus, equipAttack)
mobDefBonus := combat.SelectBonus(attackType,
mob.StabDefense, mob.SlashDefense, mob.CrushDefense,
@@ -331,8 +353,8 @@ func (g *Game) calculateUnarmedAttackRoll(p *player.Player, mob *world.MobInstan
weaponType = object.WeaponMelee
attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle))
- effectiveAttack := p.Level(player.Attack) + g.techLevelBonus(p, "attack") + g.buffLevelBonus(p, "attack")
- attRoll = combat.EffectiveRoll(effectiveAttack, attBonus, 0)
+ effectiveAccuracy := p.Level(player.Accuracy) + g.techLevelBonus(p, "accuracy") + g.buffLevelBonus(p, "accuracy")
+ attRoll = combat.EffectiveRoll(effectiveAccuracy, attBonus, 0)
mobDefBonus := combat.SelectBonus(attackType,
mob.StabDefense, mob.SlashDefense, mob.CrushDefense,
@@ -374,30 +396,34 @@ func (g *Game) applyPlayerHit(sess *net.Session, p *player.Player, mob *world.Mo
g.consumeAmmo(sess, p)
}
- 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, 999))
- if w2 := len(fmt.Sprintf("%s hits you for %d damage.", attacker, 999)); w2 > w {
- w = w2
- }
- prefix := fmt.Sprintf("You hit %s for %s damage.", g.colorize(sess, "mob", mobName), g.colorize(sess, "damage_dealt", 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, mob.HP, mob.MaxHP), mob.HP, mob.MaxHP)
- line := prefix + hpSuffix
- if p.OptionBool("xp_drops") && 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)]))
+ if mob.IsTask() {
+ g.writeTaskProgress(sess, p, mob, dmg, gains)
+ } else {
+ 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, 999))
+ if w2 := len(fmt.Sprintf("%s hits you for %d damage.", attacker, 999)); w2 > w {
+ w = w2
+ }
+ prefix := fmt.Sprintf("You hit %s for %s damage.", g.colorize(sess, "mob", mobName), g.colorize(sess, "damage_dealt", 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, mob.HP, mob.MaxHP), mob.HP, mob.MaxHP)
+ line := prefix + hpSuffix
+ if p.OptionBool("xp_drops") && 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 += g.colorize(sess, "xp", " ("+strings.Join(parts, ", ")+")")
}
- line += g.colorize(sess, "xp", " ("+strings.Join(parts, ", ")+")")
+ sess.WriteLine(line)
}
- sess.WriteLine(line)
if isRanged && p.AmmoQty <= 0 {
sess.WriteLine("You've run out of ammo!")
@@ -559,60 +585,51 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
p.ActionState = nil
if p.HP <= 0 {
- 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()
- g.safespotMu.Lock()
- if ss, ok := g.safespotStates[p.Name]; ok {
- g.safespotMu.Unlock()
- g.forceLeaveSafespot(sess, p, ss, "")
- } else {
- g.safespotMu.Unlock()
- }
- 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)
- g.writePrompt(sess)
+ g.killPlayer(sess, p, mob)
return
}
if mob != nil && mob.HP <= 0 {
+ isTask := mob.IsTask()
p.Stats.RecordMobKill(mob.DefID)
- sess.WriteLine(g.colorize(sess, "victory", fmt.Sprintf("\nYou have defeated %s!", mobDisplayName(mob, true))))
- g.onAssassinKill(sess, p, mob)
+ 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 {
- 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)))
+ 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)
@@ -620,12 +637,8 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
if def != nil {
name = def.Name
}
- dropper := mob.Name
- if !mob.Unique {
- dropper = "The " + mob.Name
- }
coloredName := g.itemColorize(sess, def, name)
- sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropper+" drops:"), coloredName))
+ sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropLabel), coloredName))
}
if len(mob.Drops.Loot) > 0 {
@@ -641,15 +654,11 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
if def != nil {
name = def.Name
}
- dropper := mob.Name
- if !mob.Unique {
- dropper = "The " + mob.Name
- }
coloredName := g.itemColorize(sess, def, name)
if qty > 1 {
- sess.WriteLine(fmt.Sprintf("%s %d x %s", g.colorize(sess, "drop_message", dropper+" drops:"), qty, coloredName))
+ 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", dropper+" drops:"), coloredName))
+ sess.WriteLine(fmt.Sprintf("%s %s", g.colorize(sess, "drop_message", dropLabel), coloredName))
}
}
}
@@ -667,6 +676,50 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
}
}
+// 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) {
+ combat.LeaveCombat(p.Name)
+ p.ActionState = nil
+
+ 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
+ g.safespotMu.Lock()
+ if ss, ok := g.safespotStates[p.Name]; ok {
+ g.safespotMu.Unlock()
+ g.forceLeaveSafespot(sess, p, ss, "")
+ } else {
+ g.safespotMu.Unlock()
+ }
+ 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
@@ -679,7 +732,7 @@ func (g *Game) awardCombatXP(sess *net.Session, p *player.Player, dmg int, isRan
} else {
switch p.AttackStyle {
case player.Accurate:
- gains = []xpGain{{string(player.Attack), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}}
+ 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:
@@ -687,7 +740,7 @@ func (g *Game) awardCombatXP(sess *net.Session, p *player.Player, dmg int, isRan
case player.Balanced:
quarter := baseXP / 4
gains = []xpGain{
- {string(player.Attack), quarter},
+ {string(player.Accuracy), quarter},
{string(player.Strength), quarter},
{string(player.Defense), quarter},
{string(player.Hitpoints), quarter},
@@ -722,6 +775,22 @@ func (g *Game) executeAttack(sess *net.Session, args []string, rawInput string)
}
}
+// executeWork is the non-violent alias of attack. It routes to the same engine;
+// doAttack auto-detects whether the target is a task worksite or a combat mob.
+func (g *Game) executeWork(sess *net.Session, args []string, rawInput string) {
+ if len(args) == 0 {
+ p := sess.Player
+ target := g.resolveDefaultMob(p.RoomID)
+ if target == "" {
+ sess.WriteLine("Work on what?")
+ return
+ }
+ g.doAttack(sess, target)
+ } else {
+ g.doAttack(sess, strings.Join(args, " "))
+ }
+}
+
func (g *Game) respawnMob(instanceID string) {
inst := g.MobStore.GetInstance(instanceID)
if inst == nil {