diff options
| author | workhorse <workhorse@localhost.localdomain> | 2026-06-19 03:57:06 -0400 |
|---|---|---|
| committer | workhorse <workhorse@localhost.localdomain> | 2026-06-19 03:57:06 -0400 |
| commit | 52a3ce6a4b4a254dc5d3067979a09e93c060fa20 (patch) | |
| tree | 167c62990013733bc8669c1387078bc86ea48db5 /internal/game/assassin.go | |
| parent | fbd1c30d7b9777c9df6653f0c393afa7a7dfa519 (diff) | |
| download | thehouseoficarus-52a3ce6a4b4a254dc5d3067979a09e93c060fa20.tar.gz | |
feat: shops and rough assassin skill
Diffstat (limited to 'internal/game/assassin.go')
| -rw-r--r-- | internal/game/assassin.go | 284 |
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) +} |
