aboutsummaryrefslogtreecommitdiff
path: root/internal/game/assassin.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/assassin.go')
-rw-r--r--internal/game/assassin.go284
1 files changed, 284 insertions, 0 deletions
diff --git a/internal/game/assassin.go b/internal/game/assassin.go
new file mode 100644
index 0000000..d7af1a4
--- /dev/null
+++ b/internal/game/assassin.go
@@ -0,0 +1,284 @@
+package game
+
+import (
+ "fmt"
+ "math/rand"
+ "strings"
+
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+func getPlayerFlagInt(p *player.Player, key string) int {
+ if p.Flags == nil {
+ return 0
+ }
+ val, ok := p.Flags[key]
+ if !ok {
+ return 0
+ }
+ switch v := val.(type) {
+ case int:
+ return v
+ case int64:
+ return int(v)
+ case float64:
+ return int(v)
+ }
+ return 0
+}
+
+func getPlayerFlagString(p *player.Player, key string) string {
+ if p.Flags == nil {
+ return ""
+ }
+ val, ok := p.Flags[key]
+ if !ok {
+ return ""
+ }
+ s, _ := val.(string)
+ return s
+}
+
+func setPlayerFlag(p *player.Player, key string, val any) {
+ if p.Flags == nil {
+ p.Flags = make(map[string]any)
+ }
+ p.Flags[key] = val
+}
+
+type assassinTaskEntry struct {
+ MobID string
+ MinLevel int
+ MaxLevel int
+ MinCount int
+ MaxCount int
+ Weight int
+}
+
+var assassinTaskTable = []assassinTaskEntry{
+ {"man", 1, 15, 10, 25, 8},
+ {"cow", 1, 15, 10, 25, 8},
+ {"slug", 1, 99, 15, 45, 15},
+ {"drone", 15, 99, 20, 50, 12},
+ {"crawler", 30, 99, 15, 40, 10},
+ {"phantom", 45, 99, 10, 30, 8},
+}
+
+func (g *Game) assignAssassinTask(sess *net.Session, p *player.Player) {
+ level := p.Level(player.Assassin)
+ var eligible []assassinTaskEntry
+ totalWeight := 0
+ for _, entry := range assassinTaskTable {
+ if level >= entry.MinLevel && level <= entry.MaxLevel {
+ eligible = append(eligible, entry)
+ totalWeight += entry.Weight
+ }
+ }
+ if len(eligible) == 0 {
+ sess.WriteLine("The Client shakes their head. \"Nothing available for your level.\"")
+ return
+ }
+ roll := rand.Intn(totalWeight)
+ var chosen assassinTaskEntry
+ for _, entry := range eligible {
+ roll -= entry.Weight
+ if roll < 0 {
+ chosen = entry
+ break
+ }
+ }
+ count := chosen.MinCount + rand.Intn(chosen.MaxCount-chosen.MinCount+1)
+ setPlayerFlag(p, "assassin_task_mob", chosen.MobID)
+ setPlayerFlag(p, "assassin_task_total", count)
+ setPlayerFlag(p, "assassin_task_remaining", count)
+ g.AccountStore.SaveCharacter(p)
+
+ def, err := g.MobStore.LoadDef(chosen.MobID)
+ name := chosen.MobID
+ if err == nil {
+ name = def.Name
+ }
+ sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf("\"Your target: %d %ss. Get to work.\"", count, name)))
+}
+
+func (g *Game) onAssassinKill(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ taskMob := getPlayerFlagString(p, "assassin_task_mob")
+ if taskMob == "" || taskMob != mob.DefID {
+ return
+ }
+ remaining := getPlayerFlagInt(p, "assassin_task_remaining")
+ if remaining <= 0 {
+ return
+ }
+ xp := mob.MaxHP * 2
+ newLevel := p.AddSkillXP(player.Assassin, xp)
+ if newLevel > 0 {
+ sess.WriteLine(g.colorize(sess, "level_up", fmt.Sprintf("*** You are now level %d Assassin! ***", newLevel)))
+ }
+ if p.OptionBool("xp_drops") {
+ sess.WriteLine(g.colorize(sess, "xp", fmt.Sprintf(" (+%dxp asm)", xp)))
+ }
+ remaining--
+ setPlayerFlag(p, "assassin_task_remaining", remaining)
+ if remaining <= 0 {
+ completed := getPlayerFlagInt(p, "assassin_tasks_completed") + 1
+ streak := getPlayerFlagInt(p, "assassin_streak") + 1
+ setPlayerFlag(p, "assassin_tasks_completed", completed)
+ setPlayerFlag(p, "assassin_streak", streak)
+ delete(p.Flags, "assassin_task_mob")
+ setPlayerFlag(p, "assassin_task_remaining", 0)
+ setPlayerFlag(p, "assassin_task_total", 0)
+ rep := 1
+ bonus := streakBonus(streak)
+ rep += bonus
+ currentRep := getPlayerFlagInt(p, "assassin_reputation")
+ setPlayerFlag(p, "assassin_reputation", currentRep+rep)
+ sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf("\n*** Assassin task complete! ***")))
+ if bonus > 0 {
+ sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf(" Streak bonus! %d tasks in a row. +%d bonus reputation.", streak, bonus)))
+ }
+ sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf(" Reputation earned: %d (total: %d)", rep, currentRep+rep)))
+ } else {
+ total := getPlayerFlagInt(p, "assassin_task_total")
+ def, _ := g.MobStore.LoadDef(taskMob)
+ name := taskMob
+ if def != nil {
+ name = def.Name
+ }
+ sess.WriteLine(g.colorize(sess, "assassin_task", fmt.Sprintf(" Assassin task: %d of %d %ss remaining.", remaining, total, name)))
+ }
+ g.AccountStore.SaveCharacter(p)
+}
+
+func (g *Game) skipAssassinTask(sess *net.Session, p *player.Player) {
+ rep := getPlayerFlagInt(p, "assassin_reputation")
+ if rep < 30 {
+ sess.WriteLine("You don't have enough Reputation to skip. (Need 30, have " + fmt.Sprint(rep) + ")")
+ return
+ }
+ setPlayerFlag(p, "assassin_reputation", rep-30)
+ delete(p.Flags, "assassin_task_mob")
+ setPlayerFlag(p, "assassin_task_remaining", 0)
+ setPlayerFlag(p, "assassin_task_total", 0)
+ setPlayerFlag(p, "assassin_streak", 0)
+ g.AccountStore.SaveCharacter(p)
+}
+
+func (g *Game) extendAssassinTask(sess *net.Session, p *player.Player) {
+ rep := getPlayerFlagInt(p, "assassin_reputation")
+ if rep < 30 {
+ sess.WriteLine("You don't have enough Reputation to extend. (Need 30, have " + fmt.Sprint(rep) + ")")
+ return
+ }
+ taskMob := getPlayerFlagString(p, "assassin_task_mob")
+ if taskMob == "" {
+ sess.WriteLine("You don't have an active task to extend.")
+ return
+ }
+ setPlayerFlag(p, "assassin_reputation", rep-30)
+ total := getPlayerFlagInt(p, "assassin_task_total")
+ remaining := getPlayerFlagInt(p, "assassin_task_remaining")
+ extension := total / 2
+ if extension < 5 {
+ extension = 5
+ }
+ setPlayerFlag(p, "assassin_task_total", total+extension)
+ setPlayerFlag(p, "assassin_task_remaining", remaining+extension)
+ g.AccountStore.SaveCharacter(p)
+
+ def, _ := g.MobStore.LoadDef(taskMob)
+ name := taskMob
+ if def != nil {
+ name = def.Name
+ }
+ sess.WriteLine(fmt.Sprintf("Task extended by %d. Kill %d more %ss (%d total).", extension, remaining+extension, name, total+extension))
+}
+
+func streakBonus(streak int) int {
+ if streak%1000 == 0 {
+ return 50
+ }
+ if streak%250 == 0 {
+ return 35
+ }
+ if streak%100 == 0 {
+ return 25
+ }
+ if streak%50 == 0 {
+ return 15
+ }
+ if streak%10 == 0 {
+ return 5
+ }
+ return 0
+}
+
+func (g *Game) tryFinishingBlow(sess *net.Session, input string) bool {
+ p := sess.Player.(*player.Player)
+ cs := combat.GetCombat(p.Name)
+ if cs == nil {
+ return false
+ }
+ mob := g.MobStore.GetInstance(cs.MobID)
+ if mob == nil || mob.FinishingBlow == "" || mob.HP != 1 {
+ return false
+ }
+ lower := strings.ToLower(input)
+ var itemPart, targetPart string
+ for _, sep := range []string{" on ", " with "} {
+ if idx := strings.Index(lower, sep); idx > 0 {
+ itemPart = strings.TrimSpace(input[:idx])
+ targetPart = strings.TrimSpace(input[idx+len(sep):])
+ break
+ }
+ }
+ if targetPart == "" {
+ itemPart = strings.TrimSpace(input)
+ if mob.MatchQuality(itemPart) != world.MatchNone {
+ targetPart = itemPart
+ itemPart = strings.TrimSpace(mob.FinishingBlow)
+ }
+ }
+ if targetPart == "" {
+ return false
+ }
+ if mob.MatchQuality(targetPart) == world.MatchNone {
+ return false
+ }
+ g.doFinishingBlow(sess, p, mob, itemPart)
+ return true
+}
+
+func (g *Game) doFinishingBlow(sess *net.Session, p *player.Player, mob *world.MobInstance, itemInput string) {
+ fbDef, err := g.ItemStore.Load(mob.FinishingBlow)
+ if err != nil {
+ sess.WriteLine("Something went wrong.")
+ return
+ }
+ if !fbDef.MatchesName(itemInput) && !strings.EqualFold(itemInput, fbDef.ID) {
+ sess.WriteLine(fmt.Sprintf("That won't work on %s. You need %s.", mobDisplayName(mob, true), fbDef.Name))
+ return
+ }
+ if !p.HasItem(mob.FinishingBlow) {
+ sess.WriteLine(fmt.Sprintf("You don't have any %s.", fbDef.Name))
+ return
+ }
+ autoKey := "assassin_unlocked_auto_" + mob.FinishingBlow
+ consumed := true
+ if p.Flags != nil {
+ if val, ok := p.Flags[autoKey]; ok {
+ if b, ok := val.(bool); ok && b {
+ consumed = false
+ }
+ }
+ }
+ if consumed {
+ p.RemoveItem(mob.FinishingBlow, 1)
+ }
+ sess.WriteLine(fmt.Sprintf("\nYou use the %s on %s!", g.itemColorize(sess, fbDef, fbDef.Name), g.colorize(sess, "mob_name", mobDisplayName(mob, true))))
+ mob.HP = 0
+ g.endCombat(sess, p, mob)
+}