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) }