diff --git a/cmd/expedition-sim/main.go b/cmd/expedition-sim/main.go index c29083c..8413c3b 100644 --- a/cmd/expedition-sim/main.go +++ b/cmd/expedition-sim/main.go @@ -48,7 +48,7 @@ func main() { userTag = flag.String("user", "@sim:expedition", "synthetic user id (single-run mode)") realUser = flag.String("real-user", "", "run an EXISTING character loaded from -data's DB instead of building a synthetic one. Pass the real mxid (e.g. @holymachina:parodia.dev). Requires -data to point at a (copy of a) populated gogobee.db dir. Heals the char to full + tops up the bankroll; leaves race/class/level/gear/spells as-is.") - logFlag = flag.Bool("log", true, "include per-row expedition log in output (single-run default true; matrix default false)") + logFlag = flag.Bool("log", true, "include per-row expedition log in output (single-run default true; matrix default false)") matrix = flag.Bool("matrix", false, "matrix mode — sweep over classes × levels × zones × runs") classes = flag.String("classes", "", "comma-separated class ids (matrix mode)") @@ -62,6 +62,7 @@ func main() { party = flag.Int("party", 1, "seat a party of N (1 = solo, the historical path). Followers are invited on Day 1 and buy their own supplies") partyClasses = flag.String("party-classes", "", "comma-separated classes for the N-1 followers (default: clones of -class)") + companion = flag.String("companion", "", "hire Pete into the party: \"auto\" fills the missing role, or name a class (cleric, fighter, …). Empty = no companion. He takes a seat but no loot/XP.") jobs = flag.Int("jobs", 0, "matrix mode — concurrent worker count (each worker is a subprocess so it gets its own sqlite). 0 = runtime.NumCPU()") ) @@ -88,7 +89,7 @@ func main() { includeLog = *logFlag } }) - runMatrix(*classes, *levels, *zones, *runs, *bank, *cap, *days, includeLog, *jobs, *trace, *petLevel, *party, *partyClasses) + runMatrix(*classes, *levels, *zones, *runs, *bank, *cap, *days, includeLog, *jobs, *trace, *petLevel, *party, *partyClasses, *companion) return } @@ -103,7 +104,7 @@ func main() { return } - runSingle(*class, *level, *zone, *userTag, *dataDir, *bank, *cap, *days, *logFlag, followers) + runSingle(*class, *level, *zone, *userTag, *dataDir, *bank, *cap, *days, *logFlag, followers, *companion) } // runReal drives an existing character loaded from dataDir's gogobee.db through @@ -161,7 +162,7 @@ func followerClasses(leaderClass string, party int, spec string) []string { return out } -func runSingle(class string, level int, zone, userTag, dataDir string, bank float64, cap, days int, includeLog bool, followers []string) { +func runSingle(class string, level int, zone, userTag, dataDir string, bank float64, cap, days int, includeLog bool, followers []string, companion string) { dir := dataDir if dir == "" { var err error @@ -172,7 +173,7 @@ func runSingle(class string, level int, zone, userTag, dataDir string, bank floa defer os.RemoveAll(dir) } - res, err := runOne(dir, id.UserID(userTag), plugin.DnDClass(class), level, plugin.ZoneID(zone), bank, cap, days, followers) + res, err := runOne(dir, id.UserID(userTag), plugin.DnDClass(class), level, plugin.ZoneID(zone), bank, cap, days, followers, companion) if err != nil { if res != nil { if !includeLog { @@ -199,7 +200,7 @@ type matrixJob struct { rep int } -func runMatrix(classes, levels, zones string, runs int, bank float64, cap, days int, includeLog bool, jobs int, trace bool, petLevel, party int, partyClasses string) { +func runMatrix(classes, levels, zones string, runs int, bank float64, cap, days int, includeLog bool, jobs int, trace bool, petLevel, party int, partyClasses, companion string) { cs := splitNonEmpty(classes) ls := parseLevels(levels) zs := splitNonEmpty(zones) @@ -230,7 +231,7 @@ func runMatrix(classes, levels, zones string, runs int, bank float64, cap, days var wg sync.WaitGroup for i := 0; i < jobs; i++ { wg.Add(1) - go matrixWorker(exe, workCh, resCh, &wg, bank, cap, days, includeLog, trace, petLevel, party, partyClasses) + go matrixWorker(exe, workCh, resCh, &wg, bank, cap, days, includeLog, trace, petLevel, party, partyClasses, companion) } go func() { for _, j := range work { @@ -249,7 +250,7 @@ func runMatrix(classes, levels, zones string, runs int, bank float64, cap, days } } -func matrixWorker(exe string, in <-chan matrixJob, out chan<- *plugin.SimResult, wg *sync.WaitGroup, bank float64, cap, days int, includeLog, trace bool, petLevel, party int, partyClasses string) { +func matrixWorker(exe string, in <-chan matrixJob, out chan<- *plugin.SimResult, wg *sync.WaitGroup, bank float64, cap, days int, includeLog, trace bool, petLevel, party int, partyClasses, companion string) { defer wg.Done() for j := range in { uid := fmt.Sprintf("@sim:%s-l%d-%s-%d", j.class, j.level, j.zone, j.rep) @@ -275,6 +276,9 @@ func matrixWorker(exe string, in <-chan matrixJob, out chan<- *plugin.SimResult, if partyClasses != "" { args = append(args, "-party-classes", partyClasses) } + if companion != "" { + args = append(args, "-companion", companion) + } if trace { args = append(args, "-trace") } @@ -314,7 +318,7 @@ func matrixWorker(exe string, in <-chan matrixJob, out chan<- *plugin.SimResult, } } -func runOne(dataDir string, uid id.UserID, class plugin.DnDClass, level int, zone plugin.ZoneID, bank float64, cap, days int, followers []string) (*plugin.SimResult, error) { +func runOne(dataDir string, uid id.UserID, class plugin.DnDClass, level int, zone plugin.ZoneID, bank float64, cap, days int, followers []string, companion string) (*plugin.SimResult, error) { runner, err := plugin.NewSimRunner(dataDir) if err != nil { return nil, fmt.Errorf("init runner: %w", err) @@ -338,6 +342,7 @@ func runOne(dataDir string, uid id.UserID, class plugin.DnDClass, level int, zon runner.Euro.Credit(muid, bank, "expedition-sim bankroll") members = append(members, muid) } + runner.Companion = companion return runner.RunPartyExpedition(uid, members, zone, cap, days) } diff --git a/internal/db/db.go b/internal/db/db.go index 7e0a893..c59ce5d 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -401,6 +401,15 @@ func runMigrations(d *sql.DB) error { // there is no read-modify-write race and no second field to disagree. // DEFAULT 0 == "no renown", correct for every pre-existing row, no bootstrap. `ALTER TABLE player_meta ADD COLUMN renown_xp INTEGER NOT NULL DEFAULT 0`, + + // The hireable companion (pete_adventure_news_plan.md, "Pete as a + // character"). Pete's roster seat records the class and level he was + // hired at, so a party that levels mid-expedition doesn't silently + // re-roll its hireling into a different class three rooms in. Both are + // empty/0 on every player row and are only ever read for the one seat + // where isCompanionSeat holds — a player's row never consults them. + `ALTER TABLE expedition_party ADD COLUMN companion_class TEXT NOT NULL DEFAULT ''`, + `ALTER TABLE expedition_party ADD COLUMN companion_level INTEGER NOT NULL DEFAULT 0`, } for _, stmt := range columnMigrations { if _, err := d.Exec(stmt); err != nil { diff --git a/internal/plugin/adventure_companion.go b/internal/plugin/adventure_companion.go new file mode 100644 index 0000000..0b83afa --- /dev/null +++ b/internal/plugin/adventure_companion.go @@ -0,0 +1,554 @@ +package plugin + +import ( + "database/sql" + "errors" + "fmt" + "os" + "strings" + "time" + + "gogobee/internal/db" + "gogobee/internal/peteclient" + + "maunium.net/go/mautrix/id" +) + +// Pete, the realm's embedded correspondent — the hireable NPC companion. +// (pete_adventure_news_plan.md, "Pete as a character", surface 2.) +// +// A leader short a body can hire Pete into an expedition party. He fills the +// role the party is missing, fights on autopilot, and files a dispatch about it +// afterwards. +// +// The load-bearing rule, and the reason this file exists rather than a +// player_meta row for @pete: **Pete is not a player and must never become one.** +// He has no player_meta, no dnd_character, no inventory, no euros, and no DM +// room. Mint him a player_meta row and ensureDnDCharacterForCombat will happily +// auto-build him a real character on his first swing, at which point he shows up +// in the graveyard, the leaderboards, the news as a subject, and the daily event +// rolls — all of which would be a bot reporting on itself. +// +// So his seat is synthesized: companionCombatant builds a Combatant in memory +// from the same tuned layers a player's sheet goes through, and every seam that +// assumes a seat is a person is guarded by isCompanionSeat. The guards live at +// four chokepoints, which between them cover the whole blast radius: +// +// - expeditionAudience — he is never in the DM fan-out (and so never in the +// per-member pet-arrival rolls or the daily event rolls that ride it) +// - partySize — he is not a mouth: he doesn't inflate the supply +// burn he never bought packs for, and an NPC-only roster doesn't lock the +// leader out of their next expedition +// - partyCombatantsForSession / the seat builders — synthesize, don't load +// - the close-out loops — no XP, no loot, no death row, no achievements +// +// He does count toward the enemy-HP scalar, because he is a body in the fight +// and the boss can feel him. + +// companionUserIDDefault is Pete's real Matrix account. He is an independent bot +// (his own repo, his own voice); gogobee already ignores him as a sender via +// IGNORED_BOTS, so seating him here can never round-trip into command handling. +// PETE_USER_ID overrides for a differently-homed deployment. +const companionUserIDDefault = "@pete:parodia.dev" + +// companionDisplayName is what the party sees on his seat. gogobee names him but +// does NOT voice him: his hire banter and his dispatch are written by his own +// bot from the fact emitted below (project_pete_bot_architecture). +const companionDisplayName = "Pete" + +// Hire pricing. The plan files cost as an open tuning question; this is the +// first answer, not the final one. It scales with both the level he shows up at +// and the tier he's walking into, so hiring him for a T5 boss run is not the +// same 300 coins as a T1 stroll. Supply packs are 50/90 coins for reference — +// he is deliberately a real expense, not a rounding error. +// +// PROD WATCH: at endgame coin balances this is likely too cheap to be a sink. +// Raise companionHireCoinsPerLevel before raising the base — the base is what a +// low-level player short a friend has to find. +const ( + companionHireBaseCoins = 300 + companionHireCoinsPerLevel = 60 +) + +// companionLevelPenalty is what makes him help rather than carry. He arrives one +// level below the party's average — a competent below-median member, per the +// difficulty plan's standing rule: lift the trailing case, never nerf the +// leaders and never touch monster scaling. +const companionLevelPenalty = 1 + +var ( + ErrCompanionAlreadyHired = errors.New("pete is already with this party") + ErrCompanionOnAssignment = errors.New("pete is out on assignment") + ErrCompanionNotHired = errors.New("pete is not with this party") +) + +// companionUserID resolves Pete's Matrix id. +func companionUserID() id.UserID { + if v := strings.TrimSpace(os.Getenv("PETE_USER_ID")); v != "" { + return id.UserID(v) + } + return id.UserID(companionUserIDDefault) +} + +// isCompanionSeat reports whether a roster seat / combat seat is Pete rather +// than a player. This is the predicate every guard in the codebase keys on. +func isCompanionSeat(userID id.UserID) bool { return userID == companionUserID() } + +// isCompanionUser is the string-keyed form, for the many seams that carry a raw +// user_id out of the database. +func isCompanionUser(userID string) bool { return id.UserID(userID) == companionUserID() } + +// companionHireCost is what the leader pays to bring him along, in coins. +func companionHireCost(level int, tier ZoneTier) int { + if level < 1 { + level = 1 + } + t := int(tier) + if t < 1 { + t = 1 + } + return (companionHireBaseCoins + companionHireCoinsPerLevel*level) * t +} + +// ── who he shows up as ─────────────────────────────────────────────────────── + +// companionRoleFill picks the class Pete plays. He is role-fluid — that is the +// whole point of hiring him — so by default he fills the hole in the roster: no +// healer, he's a Cleric; no damage, he's a Mage; nobody up front, he's a +// Fighter. A leader who knows better can override. +// +// The order of the checks is the priority order: a party with neither a healer +// nor a front line gets the healer, because a party that cannot heal is the one +// that dies. +func companionRoleFill(partyClasses []DnDClass) DnDClass { + var hasHealer, hasFront, hasDamage bool + for _, c := range partyClasses { + switch c { + case ClassCleric, ClassDruid, ClassBard: + hasHealer = true + case ClassPaladin: + // The one chassis that answers two questions at once. + hasHealer, hasFront = true, true + case ClassFighter: + hasFront = true + case ClassMage, ClassSorcerer, ClassWarlock, ClassRogue, ClassRanger: + hasDamage = true + } + } + switch { + case !hasHealer: + return ClassCleric + case !hasFront: + return ClassFighter + case !hasDamage: + return ClassMage + default: + // A complete party that hires him anyway gets a second pair of hands up + // front — the least redundant thing he can be. + return ClassFighter + } +} + +// parseCompanionClass resolves an explicit `!expedition hire cleric` override. +// Empty (or unknown) means auto-fill. +func parseCompanionClass(arg string) (DnDClass, bool) { + arg = strings.ToLower(strings.TrimSpace(arg)) + if arg == "" || arg == "auto" { + return "", false + } + for _, ci := range dndClasses { + if !ci.Playable { + continue + } + if string(ci.Key) == arg || strings.EqualFold(ci.Display, arg) { + return ci.Key, true + } + } + return "", false +} + +// companionHumans is every *player* on the expedition — the roster if one exists, +// and otherwise the owner alone. +// +// The fallback is the whole point. A solo expedition has NO expedition_party rows +// (see partyMembers: absence means solo, and the roster only materializes on the +// first successful invite). Reading the roster alone therefore answers "nobody" for +// exactly the player this feature exists for: the one with no friends around, who +// is hiring Pete *because* they are alone. +// +// Getting this wrong is not a small error. It hired every solo player a **level-1** +// Pete — in a tier-4 zone, against a boss that had just gained 15% HP and a full +// extra set of actions to account for him. He died on contact and left the leader +// fighting an inflated boss alone. A 1500-run sweep measured it: solo 65% clear, +// two humans 87%, solo+Pete 33%. The companion was worse than no companion, and +// this line is why. +func companionHumans(expeditionID string) []*DnDCharacter { + var owner string + if err := db.Get().QueryRow( + `SELECT user_id FROM dnd_expedition WHERE expedition_id = ?`, + expeditionID).Scan(&owner); err != nil { + return nil + } + seats, err := partyHumans(expeditionID, owner) + if err != nil { + return nil + } + out := make([]*DnDCharacter, 0, len(seats)) + for _, s := range seats { + if dc, _ := LoadDnDCharacter(s.UserID); dc != nil { + out = append(out, dc) + } + } + return out +} + +// companionPartyLevel is the level he arrives at: the party's average, less +// companionLevelPenalty, floored at 1. +func companionPartyLevel(expeditionID string) int { + chars := companionHumans(expeditionID) + if len(chars) == 0 { + return 1 + } + sum := 0 + for _, dc := range chars { + sum += dc.Level + } + lvl := sum/len(chars) - companionLevelPenalty + if lvl < 1 { + lvl = 1 + } + return lvl +} + +// companionPartyClasses reads the classes already on the expedition, for the role +// fill. Solo resolves to the owner's class, so a lone fighter gets a healer rather +// than the empty-party default. +func companionPartyClasses(expeditionID string) []DnDClass { + chars := companionHumans(expeditionID) + out := make([]DnDClass, 0, len(chars)) + for _, dc := range chars { + out = append(out, dc.Class) + } + return out +} + +// ── his sheet, which lives only in memory ──────────────────────────────────── + +// companionSheet synthesizes the DnDCharacter Pete fights as. It is built, used, +// and thrown away inside a single combat build — it is never saved, and +// SaveDnDCharacter must never be called on it. +// +// Stats come from the same class-priority + race-mod pipeline autoBuildCharacter +// uses for a real character, so he is statted like a player of his level rather +// than by a bespoke NPC table that would drift away from the tuned math. Human +// is deliberate: the +1-to-all is the most neutral race in the book, so his +// class is doing the work rather than a race pick nobody chose. +func companionSheet(class DnDClass, level int) *DnDCharacter { + if level < 1 { + level = 1 + } + scores := applyRaceMods(RaceHuman, classStatPriority(class)) + c := &DnDCharacter{ + UserID: companionUserID(), + Race: RaceHuman, + Class: class, + Level: level, + STR: scores[0], DEX: scores[1], CON: scores[2], + INT: scores[3], WIS: scores[4], CHA: scores[5], + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + } + c.HPMax = computeMaxHP(c.Class, abilityModifier(c.CON), c.Level) + c.HPCurrent = c.HPMax + return c +} + +// companionAdvCharacter is the AdventureCharacter half of the synthetic sheet: +// the shim DerivePlayerStats needs. CombatLevel round-trips back to the D&D +// level through dndLevelFromCombatLevel (level*5), so the two halves agree. +func companionAdvCharacter(level int) *AdventureCharacter { + return &AdventureCharacter{ + UserID: companionUserID(), + DisplayName: companionDisplayName, + CombatLevel: level * 5, + Alive: true, + } +} + +// companionGearTier maps his level onto the equipment tier a player of that +// level would plausibly be carrying: 1–4 → T1, 5–8 → T2, and so on to T5. +func companionGearTier(level int) int { + t := (level + 3) / 4 + if t < 1 { + t = 1 + } + if t > 5 { + t = 5 + } + return t +} + +// companionGear is the kit he walks in with — a working reporter's kit, bought +// with expenses and kept in serviceable shape. +// +// He has to have one. The gear layer is not decoration: unarmored, stats.AC is +// never set at all (computeArmorAC only fires when armor exists) and he walks in +// at AC 3, hit by everything; weaponless, stats.Weapon stays nil and he swings +// for a flat 5 at every level, which by L14 is nothing. "No gear" is not a +// below-median player, it is a broken one. +// +// The below-median comes from everywhere else: he is a level down, his gear is +// never Masterwork, and he carries no magic items, no subclass and no armed +// ability. The weapon names are chosen to hit the right branch of +// synthesizeWeaponProfile for the class — it best-fits off the name. +func companionGear(class DnDClass, level int) map[EquipmentSlot]*AdvEquipment { + tier := companionGearTier(level) + + weapon := "Service Mace" + switch class { + case ClassFighter, ClassPaladin: + weapon = "Service Sword" + case ClassRogue: + weapon = "Service Dagger" + case ClassRanger: + weapon = "Service Bow" + case ClassMage, ClassSorcerer, ClassWarlock, ClassDruid, ClassBard: + weapon = "Service Staff" + } + + return map[EquipmentSlot]*AdvEquipment{ + SlotWeapon: {Slot: SlotWeapon, Tier: tier, Condition: 100, Name: weapon}, + SlotArmor: {Slot: SlotArmor, Tier: tier, Condition: 100, Name: "Service Kit"}, + } +} + +// companionCombatant builds Pete's seat for a fight, mirroring buildZoneCombatants +// but sourcing the sheet from memory instead of the database. He carries no +// equipment, no treasure bonuses, no magic items, no chat level and no streak — +// the three layers a player accumulates and he never will. That absence *is* the +// below-median: he is a bare class chassis at a level below yours, and the gap +// between him and a geared player of the same level is exactly the gear. +func (p *AdventurePlugin) companionCombatant( + class DnDClass, level int, monster DnDMonsterTemplate, tier int, dmMood int, +) (Combatant, Combatant, *DnDCharacter) { + tilt := dmMoodCombatTilt(dmMood) + char := companionAdvCharacter(level) + dc := companionSheet(class, level) + + // The layer order is buildZoneCombatants', deliberately — a companion statted + // by a different pipeline would drift away from the tuned math the moment + // anyone touched one and not the other. + // + // What he does NOT get is the subclass layer, magic items, and an armed + // ability: three of the things a player accumulates and a hireling never will. + // Those absences, plus the level penalty and gear that is never Masterwork, + // are the "below median" — see companionGear for why the gear itself is not + // one of the things we take away. + gear := companionGear(class, level) + stats, mods := DerivePlayerStats(char, gear, &AdvBonusSummary{}, 0, 0, false) + applyDnDPlayerLayer(&stats, dc) + applyDnDEquipmentLayer(&stats, dc, gear) + applyDnDHPScaling(&stats, dc) + applyClassPassives(&stats, &mods, dc) + applyRacePassives(&stats, &mods, dc) + + enemyStats, enemyMods := monster.toCombatStats() + if tier > 1 { + if floorAC := dndDungeonACBase + tier; enemyStats.AC < floorAC { + enemyStats.AC = floorAC + } + if floorAB := dndDungeonAtkBase + tier; enemyStats.AttackBonus < floorAB { + enemyStats.AttackBonus = floorAB + } + } + enemyStats.Attack += tilt.EnemyAttackDelta + if enemyStats.Attack < 1 { + enemyStats.Attack = 1 + } + mods.InitiativeBias += tilt.InitiativeBias + + player := Combatant{ + Name: companionDisplayName, + Stats: stats, + Mods: mods, + IsPlayer: true, + } + enemy := Combatant{ + Name: monster.Name, + Stats: enemyStats, + Mods: enemyMods, + Ability: monster.Ability, + } + return player, enemy, dc +} + +// companionRosterLine is how he reads on `!expedition party`. DisplayName would +// come back empty for him — he has no player_meta row to hold a name — so the +// roster names him here, along with what he is currently playing, because "Pete +// (member)" tells the leader nothing about the hole they paid to fill. +func companionRosterLine(expeditionID string) string { + class, level := companionLoadout(expeditionID) + ci, _ := classInfo(class) + return fmt.Sprintf("**%s** _(hired — level %d %s)_\n", companionDisplayName, level, ci.Display) +} + +// ── the hire, persisted on the roster ──────────────────────────────────────── + +// companionLoadout reads back the class and level he was hired at. It is stored +// on the roster row rather than re-derived per fight, so a party that levels +// mid-expedition doesn't quietly re-roll their hireling into a different class +// three rooms in. +func companionLoadout(expeditionID string) (DnDClass, int) { + var class string + var level int + err := db.Get().QueryRow(` + SELECT companion_class, companion_level + FROM expedition_party + WHERE expedition_id = ? AND user_id = ?`, + expeditionID, string(companionUserID())).Scan(&class, &level) + if err != nil || class == "" { + return ClassFighter, 1 + } + return DnDClass(class), level +} + +// companionLoadoutForRun is companionLoadout keyed by the zone run instead of +// the expedition. A CombatSession carries a RunID, not an expedition id, and the +// per-turn rebuild is the hottest caller — so the join lives here rather than +// making every caller resolve the expedition first. +// +// A run with no expedition (a standalone !zone run, which can have no companion) +// finds no row and falls back, which is the correct reading. +func companionLoadoutForRun(runID string) (DnDClass, int) { + var class string + var level int + err := db.Get().QueryRow(` + SELECT p.companion_class, p.companion_level + FROM expedition_party p + JOIN dnd_expedition e ON e.expedition_id = p.expedition_id + WHERE e.run_id = ? AND p.user_id = ?`, + runID, string(companionUserID())).Scan(&class, &level) + if err != nil || class == "" { + return ClassFighter, 1 + } + return DnDClass(class), level +} + +// companionExpeditionFor resolves the expedition a leader is running, for the +// seat builder — which has the roster but not the expedition id. Empty string +// when there is none, which companionLoadout reads as "no row" and falls back. +func companionExpeditionFor(leader id.UserID) string { + e, _, err := activeExpeditionFor(leader) + if err != nil || e == nil { + return "" + } + return e.ID +} + +// companionSeated reports whether Pete is on this roster. +func companionSeated(expeditionID string) bool { + var one int + err := db.Get().QueryRow(` + SELECT 1 FROM expedition_party WHERE expedition_id = ? AND user_id = ?`, + expeditionID, string(companionUserID())).Scan(&one) + return err == nil +} + +// hireCompanion seats Pete. The seat check, the availability check and the +// insert share a transaction, so two leaders racing for him cannot both win. +// +// He is globally exclusive — one party at a time. That is not a limitation to +// route around: "Pete is out on assignment" is the scarcity knob the plan asks +// for, and it is also why he cannot be the answer to every run. +func hireCompanion(expeditionID string, class DnDClass, level int) error { + tx, err := db.Get().Begin() + if err != nil { + return err + } + defer tx.Rollback() + + if err := seatLeader(tx, expeditionID); err != nil { + return err + } + + // Is he already out with somebody? Scoped to live expeditions, so a roster + // row stranded by a crash cannot make him unhireable forever. + var busyOn string + err = tx.QueryRow(` + SELECT p.expedition_id + FROM expedition_party p + JOIN dnd_expedition e ON e.expedition_id = p.expedition_id + WHERE p.user_id = ? AND e.status IN ('active', 'extracting') + LIMIT 1`, string(companionUserID())).Scan(&busyOn) + switch { + case err == nil && busyOn == expeditionID: + return ErrCompanionAlreadyHired + case err == nil: + return ErrCompanionOnAssignment + case !errors.Is(err, sql.ErrNoRows): + return err + } + + var n int + if err := tx.QueryRow( + `SELECT COUNT(*) FROM expedition_party WHERE expedition_id = ?`, + expeditionID).Scan(&n); err != nil { + return err + } + if n >= expeditionPartyMax { + return ErrPartyFull + } + + if _, err := tx.Exec(` + INSERT INTO expedition_party (expedition_id, user_id, role, companion_class, companion_level) + VALUES (?, ?, 'member', ?, ?)`, + expeditionID, string(companionUserID()), string(class), level); err != nil { + return fmt.Errorf("seat companion: %w", err) + } + return tx.Commit() +} + +// dismissCompanion sends him home. Unlike a player member he can be removed +// mid-run — he is staff, not a guest — but the fee is not refunded: he already +// walked in. +func dismissCompanion(expeditionID string) error { + res, err := db.Get().Exec(` + DELETE FROM expedition_party + WHERE expedition_id = ? AND user_id = ?`, + expeditionID, string(companionUserID())) + if err != nil { + return err + } + if n, _ := res.RowsAffected(); n == 0 { + return ErrCompanionNotHired + } + return nil +} + +// ── the dispatch ───────────────────────────────────────────────────────────── + +// emitCompanionHireFact tells Pete's bot he has been hired. gogobee states the +// fact; Pete writes the sentence. Bulletin, not priority: a hire is a diary +// entry, not a bulletin-interrupting event. +// +// The subject is the *leader*, so the leader's news opt-out is honoured — Pete +// reporting "filled in as cleric for " would +// be exactly the leak the opt-out exists to prevent. Pete himself is never a +// subject: he has no opt-out row and needs none. +func emitCompanionHireFact(leader id.UserID, class DnDClass, level int, zone ZoneDefinition) { + name := charName(leader) + if name == "" { + return + } + ci, _ := classInfo(class) + emitFact(peteclient.Fact{ + GUID: "companion_hire:" + eventToken(leader, "companion_hire") + ":" + fmt.Sprint(time.Now().UTC().Unix()), + EventType: "companion_hire", + Tier: "bulletin", + Subject: name, + Zone: zone.Display, + Level: level, + ClassRace: ci.Display, + OccurredAt: time.Now().UTC().Unix(), + }, leader, "") +} diff --git a/internal/plugin/adventure_companion_test.go b/internal/plugin/adventure_companion_test.go new file mode 100644 index 0000000..6a1c7ef --- /dev/null +++ b/internal/plugin/adventure_companion_test.go @@ -0,0 +1,309 @@ +package plugin + +import ( + "errors" + "testing" + + "maunium.net/go/mautrix/id" +) + +// The companion's whole contract is "he fights, and he is not a player". These +// tests pin both halves — and specifically the seams where an NPC seat would +// otherwise be silently treated as a person. + +func TestCompanion_HiredSeatIsNotAMouth(t *testing.T) { + setupEmptyTestDB(t) + owner := id.UserID("@leader:example.org") + seedExpedition(t, "exp-hire", owner, "active") + seatLeaderFixture(t, "exp-hire") + + if err := hireCompanion("exp-hire", ClassCleric, 4); err != nil { + t.Fatalf("hireCompanion: %v", err) + } + + // The roster holds two seats... + members, err := partyMembers("exp-hire") + if err != nil { + t.Fatal(err) + } + if len(members) != 2 { + t.Fatalf("roster has %d seats, want 2 (leader + companion)", len(members)) + } + + // ...but only one of them eats. partySize feeds the daily supply burn and the + // "your party is still waiting on you" lock-out; counting the companion would + // bill the leader for rations he never bought, and strand him out of his next + // expedition behind a party of one bot. + n, err := partySize("exp-hire") + if err != nil { + t.Fatal(err) + } + if n != 1 { + t.Errorf("partySize = %d, want 1 — the companion is not a mouth", n) + } +} + +func TestCompanion_GetsNoMailButTakesASeat(t *testing.T) { + setupEmptyTestDB(t) + owner := id.UserID("@leader2:example.org") + seedExpedition(t, "exp-mail", owner, "active") + seatLeaderFixture(t, "exp-mail") + if err := hireCompanion("exp-mail", ClassFighter, 3); err != nil { + t.Fatalf("hireCompanion: %v", err) + } + exp, err := getExpedition("exp-mail") + if err != nil || exp == nil { + t.Fatalf("getExpedition: %v", err) + } + + // Mail and seats are different sets. Every DM seam reads the audience; the + // combat roster reads the seats. Getting this backwards either DMs a bot or + // charges a leader for a body that never sits down. + for _, uid := range expeditionAudience(exp) { + if isCompanionSeat(uid) { + t.Fatal("companion is in the DM audience — he does not get mail") + } + } + var seated bool + for _, uid := range expeditionSeats(exp) { + if isCompanionSeat(uid) { + seated = true + } + } + if !seated { + t.Fatal("companion is not in the fight roster — he was paid for and never sat down") + } +} + +func TestCompanion_IsGloballyExclusive(t *testing.T) { + setupEmptyTestDB(t) + for _, e := range []struct { + id string + owner id.UserID + }{{"exp-a", "@a:example.org"}, {"exp-b", "@b:example.org"}} { + seedExpedition(t, e.id, e.owner, "active") + seatLeaderFixture(t, e.id) + } + + if err := hireCompanion("exp-a", ClassMage, 5); err != nil { + t.Fatalf("first hire: %v", err) + } + // He is one person. A second party cannot have him — "out on assignment" is + // the scarcity knob, not a bug to route around. + if err := hireCompanion("exp-b", ClassMage, 5); !errors.Is(err, ErrCompanionOnAssignment) { + t.Errorf("second hire err = %v, want ErrCompanionOnAssignment", err) + } + // Re-hiring him into the party he's already with is its own answer. + if err := hireCompanion("exp-a", ClassMage, 5); !errors.Is(err, ErrCompanionAlreadyHired) { + t.Errorf("re-hire err = %v, want ErrCompanionAlreadyHired", err) + } + + // Dismissed, he's available again. + if err := dismissCompanion("exp-a"); err != nil { + t.Fatalf("dismiss: %v", err) + } + if err := hireCompanion("exp-b", ClassMage, 5); err != nil { + t.Errorf("hire after dismiss: %v, want success", err) + } +} + +// The bug that made the whole feature worse than useless: a SOLO expedition has +// no expedition_party rows at all (the roster only materializes on the first +// invite), so reading the roster to size the companion answered "nobody" for +// exactly the player who is hiring him — the one with no friends around. Every +// solo hire got a level-1 Pete, in whatever tier the leader was actually in. +func TestCompanion_SoloLeaderSizesHimCorrectly(t *testing.T) { + setupEmptyTestDB(t) + owner := id.UserID("@lonely:example.org") + seedExpedition(t, "exp-solo-hire", owner, "active") + + // A level-12 fighter, adventuring alone. No roster rows exist. + if err := SaveDnDCharacter(&DnDCharacter{ + UserID: owner, Race: RaceHuman, Class: ClassFighter, Level: 12, + STR: 18, DEX: 14, CON: 16, INT: 10, WIS: 10, CHA: 10, + HPMax: 120, HPCurrent: 120, ArmorClass: 18, + }); err != nil { + t.Fatal(err) + } + + if got := companionPartyLevel("exp-solo-hire"); got != 11 { + t.Errorf("companionPartyLevel = %d, want 11 (the lone leader's 12, less the below-median step). "+ + "A level-1 companion in the leader's zone is worse than no companion at all.", got) + } + // And he fills the hole the lone fighter actually has, rather than defaulting + // as if the party were empty. + if got := companionRoleFill(companionPartyClasses("exp-solo-hire")); got != ClassCleric { + t.Errorf("role fill for a lone fighter = %v, want cleric", got) + } +} + +// §4 — asking for "the party" must never answer "nobody". +// +// A solo expedition has no expedition_party rows (absence means solo; the roster +// materializes on the first invite). Every consumer that read the roster table +// directly therefore got an empty list for a solo player and fell back to +// whatever looked reasonable locally. That is how the companion was hired at +// level 1 for exactly the player the feature exists for. +// +// expeditionParty always includes the owner. If this test ever fails, that +// guarantee is gone and the same class of bug is back. +func TestParty_SoloExpeditionStillHasAParty(t *testing.T) { + setupEmptyTestDB(t) + owner := id.UserID("@alone:example.org") + seedExpedition(t, "exp-alone", owner, "active") + + // No roster rows exist at all. + rows, err := partyMembers("exp-alone") + if err != nil { + t.Fatal(err) + } + if len(rows) != 0 { + t.Fatalf("solo expedition has %d roster rows; the premise of this test is gone", len(rows)) + } + + // And yet the party is not empty. + seats, err := expeditionParty("exp-alone", string(owner)) + if err != nil { + t.Fatal(err) + } + if len(seats) != 1 || seats[0].UserID != owner || seats[0].Kind != SeatLeader { + t.Fatalf("expeditionParty on a solo run = %+v, want exactly the owner as leader. "+ + "An empty answer here is what hires a level-1 companion.", seats) + } + humans, err := partyHumans("exp-alone", string(owner)) + if err != nil || len(humans) != 1 { + t.Fatalf("partyHumans on a solo run = %+v (err %v), want the owner", humans, err) + } +} + +func TestCompanion_RoleFillTakesTheHole(t *testing.T) { + tests := []struct { + name string + party []DnDClass + want DnDClass + }{ + {"no healer", []DnDClass{ClassFighter, ClassRogue}, ClassCleric}, + {"no front line", []DnDClass{ClassCleric, ClassMage}, ClassFighter}, + {"no damage", []DnDClass{ClassCleric, ClassFighter}, ClassMage}, + // A paladin covers healer AND front line, so paladin+rogue has no hole at + // all — it falls through to the default, a second body up front. + {"a complete party gets the default", []DnDClass{ClassPaladin, ClassRogue}, ClassFighter}, + {"a paladin still leaves the damage hole", []DnDClass{ClassPaladin}, ClassMage}, + {"solo fighter needs the medic first", []DnDClass{ClassFighter}, ClassCleric}, + {"empty party", nil, ClassCleric}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := companionRoleFill(tc.party); got != tc.want { + t.Errorf("companionRoleFill(%v) = %v, want %v", tc.party, got, tc.want) + } + }) + } +} + +// §3 — an engine-driven seat's latch is permanent, and no command can take it. +// +// This is the invariant that, when it did not exist, made the companion stand in +// fights doing nothing. The expedition autopilot drives a party by dispatching +// each seat's turn AS that seat; his own auto-played move therefore arrived at +// beginCombatTurn looking exactly like a human returning to the keyboard, and the +// "they typed, so they're here" branch cleared the latch that was moving him. +// After round 1 he never acted again — while the boss he had inflated by 15% HP +// killed the party. The sweep measured it at -27pp clear rate. +// +// Autopilot is provisional (a human is away; a keystroke ends it). EngineDriven is +// not (there is nobody to come back). They must never collapse into one flag. +func TestCompanion_EngineSeatKeepsTheWheel(t *testing.T) { + setupEmptyTestDB(t) + p := &AdventurePlugin{} + uid := id.UserID("@lead:example.org") + + monster := dndBestiary["goblin"] + c, _, _ := p.companionCombatant(ClassFighter, 8, monster, 2, 0) + enemy := Combatant{Name: "x", Stats: CombatStats{MaxHP: 500, AC: 12, Attack: 4, AttackBonus: 4}} + + sess, err := p.startPartyCombatSession("run-e", "enc", "goblin", &enemy, []CombatSeatSetup{ + {UserID: uid, HP: 200, HPMax: 200, Mods: c.Mods, C: &c}, + {UserID: companionUserID(), HP: 120, HPMax: 120, Mods: c.Mods, C: &c, EngineDriven: true}, + }) + if err != nil { + t.Fatal(err) + } + + if !sess.seatIsEngineDriven(1) { + t.Fatal("companion seat is not engine-driven") + } + if sess.seatIsEngineDriven(0) { + t.Fatal("the human's seat came out engine-driven") + } + // The engine drives it without waiting on anybody... + if !sess.seatIsAutopiloted(1) || !sess.seatNeedsNoHuman(1) { + t.Fatal("an engine seat must resolve without waiting for a human") + } + // ...and the human's seat still waits for its human. + if sess.seatIsAutopiloted(0) { + t.Fatal("the human's seat was latched onto autopilot at seating") + } + + // The wheel cannot be taken back, because there is nobody to take it. This is + // the exact line that used to strand him: a "keystroke" from his own id. + sess.actorStatusesPtr(1).Autopilot = false + if !sess.seatIsAutopiloted(1) { + t.Fatal("clearing Autopilot stranded the engine seat — EngineDriven must keep it moving") + } +} + +// The one that actually matters: he has to FIGHT. Every other guard in this +// file is about keeping him out of things, and a companion synthesized down to +// zeroed stats would pass all of them while standing in the fight doing nothing +// — a hire that silently buys the leader an empty chair. +func TestCompanion_ActuallySwings(t *testing.T) { + setupEmptyTestDB(t) + p := &AdventurePlugin{} + + monster := dndBestiary["goblin"] + tank := monster + tank.HP = 5000 // outlast the phase clock, so a real swing has time to land + + pete, _, _ := p.companionCombatant(ClassFighter, 6, tank, 2, 0) + + if pete.Stats.MaxHP <= 0 || pete.Stats.Attack <= 0 || pete.Stats.AC <= 0 { + t.Fatalf("companion built with dead stats (%d HP / %d atk / %d AC) — he'd stand in the fight and do nothing", + pete.Stats.MaxHP, pete.Stats.Attack, pete.Stats.AC) + } + + enemy := Combatant{Name: tank.Name, Stats: CombatStats{MaxHP: 5000, AC: 10, Attack: 1, AttackBonus: 1}} + res := simulatePartyWithRNG([]Combatant{pete}, enemy, dungeonCombatPhases, seededRNG(7)) + + var swings int + for _, e := range res.Events { + if e.Actor == "player" && e.Seat == 0 && e.Roll > 0 { + swings++ + } + } + if swings == 0 { + t.Fatal("the companion never swung — he was hired, seated, and did nothing") + } +} + +func TestCompanion_SheetIsBelowMedianAndNeverPersisted(t *testing.T) { + setupEmptyTestDB(t) + + // He is statted like a player of his level, so he never drifts from the tuned + // math — but he arrives a level down, which is the whole "help, never a carry" + // rule expressed in one number. + dc := companionSheet(ClassFighter, 7) + if dc.Level != 7 || dc.HPMax <= 0 { + t.Fatalf("companionSheet = level %d / %d HP, want a real level-7 chassis", dc.Level, dc.HPMax) + } + + // And he leaves no trace: a player_meta row is the thing that would turn him + // into a real character everywhere (graveyard, news, XP, leaderboards), so the + // synthesis must not have written one. + if c, _ := LoadDnDCharacter(companionUserID()); c != nil { + t.Fatal("companion has a persisted dnd_character row — he is a player now, which is the one thing he must never be") + } + if _, err := loadAdvCharacter(companionUserID()); err == nil { + t.Fatal("companion has a persisted player_meta row — ensureDnDCharacterForCombat will auto-build him a character on his first swing") + } +} diff --git a/internal/plugin/combat_ally_heal_test.go b/internal/plugin/combat_ally_heal_test.go new file mode 100644 index 0000000..7046f83 --- /dev/null +++ b/internal/plugin/combat_ally_heal_test.go @@ -0,0 +1,174 @@ +package plugin + +import ( + "strings" + "testing" + + "maunium.net/go/mautrix/id" +) + +// §1 — the cleric fix. +// +// Until this landed, EVERY heal in the combat engine was self-scoped: +// MistyHealProc healed the actor, HealItem fired the actor's own trigger, and +// turnActionEffect.PlayerHeal wrote to the acting seat. There was no action of any +// kind that could touch another seat's HP. A party cleric — the class whose entire +// identity is keeping other people upright — could not put one hit point on a +// friend, and N3 shipped that way without a single test going red, because party +// combat had no golden. +// +// These tests exist so that can never be true again. + +// startAllyHealFight seats a healer at 0 and a hurt friend at 1. +func startAllyHealFight(t *testing.T, p *AdventurePlugin, hurtHP int) *CombatSession { + t.Helper() + healer := basePlayer() + friend := basePlayer() + enemy := Combatant{Name: "dummy", Stats: CombatStats{MaxHP: 800, AC: 10, Attack: 1, AttackBonus: 1}} + + // Distinct ids per fight: one active session per player is enforced, so two + // fights sharing a healer would collide. + tag := strings.ReplaceAll(t.Name(), "/", "_") + sess, err := p.startPartyCombatSession("run-"+tag, "enc", "goblin", &enemy, []CombatSeatSetup{ + {UserID: healerID(tag), HP: 100, HPMax: 100, Mods: healer.Mods, C: &healer}, + {UserID: friendID(tag), HP: hurtHP, HPMax: 100, Mods: friend.Mods, C: &friend}, + }) + if err != nil { + t.Fatal(err) + } + return sess +} + +func healerID(tag string) id.UserID { return id.UserID("@healer-" + tag + ":example.org") } +func friendID(tag string) id.UserID { return id.UserID("@friend-" + tag + ":example.org") } + +func TestAllyHeal_LandsOnTheFriendNotTheCaster(t *testing.T) { + setupEmptyTestDB(t) + p := &AdventurePlugin{} + sess := startAllyHealFight(t, p, 20) + + healer, friend := basePlayer(), basePlayer() + players := []*Combatant{&healer, &friend} + enemy := Combatant{Name: "dummy", Stats: CombatStats{MaxHP: 800, AC: 10, Attack: 1, AttackBonus: 1}} + ct := &combatTurn{sess: sess, players: players, enemy: &enemy, seat: 0, uid: healerID(strings.ReplaceAll(t.Name(), "/", "_"))} + + casterBefore, friendBefore := sess.seatHP(0), sess.seatHP(1) + + _, err := p.driveCombatRound(ct, PlayerAction{ + Kind: ActionCast, + Effect: &turnActionEffect{ + Label: "Cure Wounds", Action: "spell_cast", + AllyHeal: 30, AllySeat: 1, + }, + }) + if err != nil { + t.Fatal(err) + } + + if got := sess.seatHP(1); got != friendBefore+30 { + t.Errorf("friend HP = %d, want %d — the heal did not reach them. "+ + "This is the bug: a cleric who cannot heal anyone but themselves.", + got, friendBefore+30) + } + if got := sess.seatHP(0); got > casterBefore { + t.Errorf("caster HP = %d (was %d) — the heal landed on the caster instead of the target", + got, casterBefore) + } +} + +// A heal cannot exceed the target's ceiling, and cannot raise the dead. Death is +// terminal for the fight — the close-out marks them, the hospital takes them — and +// a heal that resurrected a corpse would quietly rewrite the loss rules every +// close-out path depends on. +func TestAllyHeal_CapsAtMaxAndWillNotRaiseTheDead(t *testing.T) { + setupEmptyTestDB(t) + p := &AdventurePlugin{} + + t.Run("caps at max", func(t *testing.T) { + sess := startAllyHealFight(t, p, 90) + healer, friend := basePlayer(), basePlayer() + ct := &combatTurn{sess: sess, players: []*Combatant{&healer, &friend}, + enemy: &Combatant{Name: "d", Stats: CombatStats{MaxHP: 800, AC: 10, Attack: 1, AttackBonus: 1}}, + seat: 0, uid: healerID(strings.ReplaceAll(t.Name(), "/", "_"))} + + if _, err := p.driveCombatRound(ct, PlayerAction{Kind: ActionCast, + Effect: &turnActionEffect{Label: "Cure", Action: "spell_cast", AllyHeal: 500, AllySeat: 1}, + }); err != nil { + t.Fatal(err) + } + if got, want := sess.seatHP(1), sess.seatHPMax(1); got > want { + t.Errorf("friend healed to %d over a max of %d", got, want) + } + }) + + t.Run("will not raise the dead", func(t *testing.T) { + sess := startAllyHealFight(t, p, 0) // seat 1 is already down + healer, friend := basePlayer(), basePlayer() + ct := &combatTurn{sess: sess, players: []*Combatant{&healer, &friend}, + enemy: &Combatant{Name: "d", Stats: CombatStats{MaxHP: 800, AC: 10, Attack: 1, AttackBonus: 1}}, + seat: 0, uid: healerID(strings.ReplaceAll(t.Name(), "/", "_"))} + + if _, err := p.driveCombatRound(ct, PlayerAction{Kind: ActionCast, + Effect: &turnActionEffect{Label: "Cure", Action: "spell_cast", AllyHeal: 50, AllySeat: 1}, + }); err != nil { + t.Fatal(err) + } + if got := sess.seatHP(1); got > 0 { + t.Errorf("a downed seat was healed to %d — healing keeps people up, it does not bring them back", got) + } + }) +} + +// The target parser resolves against the people in the fight, and leaves anything +// it does not recognise on the string for the spell parser — so every existing +// `!cast` form still means what it always meant. +func TestSplitCastTarget(t *testing.T) { + setupEmptyTestDB(t) + p := &AdventurePlugin{} + sess := startAllyHealFight(t, p, 50) + healer, friend := basePlayer(), basePlayer() + healer.Name, friend.Name = "Ayla", "Bram" + ct := &combatTurn{sess: sess, players: []*Combatant{&healer, &friend}, + enemy: &Combatant{Name: "d"}, seat: 0, uid: healerID(strings.ReplaceAll(t.Name(), "/", "_"))} + + tName := t.Name() + tests := []struct { + name string + in string + wantArgs string + wantSeat int + wantError bool + }{ + {"no target", "cure wounds", "cure wounds", -1, false}, + {"slot level is not a target", "fireball 3", "fireball 3", -1, false}, + {"@mention by display name", "cure wounds @Bram", "cure wounds", 1, false}, + // The flag !help has advertised all along, and that parseCombatCast used to + // accept and silently throw away ("reserved for SP3"). + {"--target flag", "cure wounds --target @Bram", "cure wounds", 1, false}, + {"--target mid-string", "cure wounds --target Bram", "cure wounds", 1, false}, + {"--target with no name", "cure wounds --target", "", -1, true}, + {"bare display name", "cure wounds Bram", "cure wounds", 1, false}, + {"by localpart", "cure wounds @" + friendID(strings.ReplaceAll(tName, "/", "_")).Localpart(), "cure wounds", 1, false}, + {"targeting yourself is just casting it", "cure wounds @Ayla", "cure wounds", -1, false}, + {"@mention of a stranger is an error", "cure wounds @nobody", "", -1, true}, + {"an unknown bare word is left for the spell parser", "mass cure wounds", "mass cure wounds", -1, false}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + args, seat, errMsg := splitCastTarget(ct, 0, tc.in) + if tc.wantError { + if errMsg == "" { + t.Fatalf("splitCastTarget(%q) gave no error; a mistyped @mention would silently waste the slot", tc.in) + } + return + } + if errMsg != "" { + t.Fatalf("splitCastTarget(%q) errored: %s", tc.in, errMsg) + } + if args != tc.wantArgs || seat != tc.wantSeat { + t.Errorf("splitCastTarget(%q) = (%q, %d), want (%q, %d)", + tc.in, args, seat, tc.wantArgs, tc.wantSeat) + } + }) + } +} diff --git a/internal/plugin/combat_cmd.go b/internal/plugin/combat_cmd.go index d110d4e..fd5afdf 100644 --- a/internal/plugin/combat_cmd.go +++ b/internal/plugin/combat_cmd.go @@ -526,10 +526,91 @@ func (p *AdventurePlugin) handleCombatCastCmd(ctx MessageContext, args string) e // the roster so the buff is live for the round's enemy turn. Before P5 that // delta went to the session's embedded copy — seat 0 — so a party member // buffing themselves would have buffed the leader. +// splitCastTarget peels an optional ally target off the end of a `!cast` argument +// — `cure wounds @alex`, or just `cure wounds alex` — and resolves it to a seat. +// +// It resolves against the people *in this fight* rather than the room, which is +// both cheaper (no ResolveUser round-trip, no RoomID to thread down here) and +// more correct: the only legal target of a combat heal is somebody sitting in the +// combat. Anything it does not recognise is left on the string for the spell +// parser, so `!cast cure wounds` and `!cast fireball 3` behave exactly as before. +// +// Both spellings work: the `--target @alex` flag that `!help` has advertised all +// along (and that parseCombatCast has been silently swallowing since SP2 — +// "reserved for SP3, accept and ignore"), and a plain trailing `@alex`. +// +// Returns (remainingArgs, seat, errMsg). seat is -1 when no target was named. +func splitCastTarget(ct *combatTurn, caster int, args string) (string, int, string) { + args = strings.TrimSpace(args) + if args == "" || !ct.isParty() { + return args, -1, "" + } + fields := strings.Fields(args) + + // `--target ` anywhere in the string. + explicit, name := false, "" + for i := 0; i < len(fields); i++ { + if !strings.EqualFold(fields[i], "--target") { + continue + } + if i+1 >= len(fields) { + return args, -1, "`--target` needs a name: `!cast cure wounds --target @alex`." + } + explicit, name = true, strings.TrimPrefix(fields[i+1], "@") + fields = append(fields[:i], fields[i+2:]...) + break + } + + if name == "" { + last := fields[len(fields)-1] + // A bare number is a slot level (`!cast fireball 3`), never a target. + if _, err := strconv.Atoi(last); err == nil { + return args, -1, "" + } + explicit = strings.HasPrefix(last, "@") + name = strings.TrimPrefix(last, "@") + if name == "" { + return args, -1, "" + } + fields = fields[:len(fields)-1] + } + // From here `fields` is the spell tokens with the target removed. + rest := strings.Join(fields, " ") + + for i, c := range ct.players { + uid := ct.sess.seatUserID(i) + if !strings.EqualFold(c.Name, name) && + !strings.EqualFold(uid, name) && + !strings.EqualFold(id.UserID(uid).Localpart(), name) { + continue + } + if i == caster { + // Targeting yourself is just casting it on yourself, which is what the + // engine does by default. Drop the target and carry on. + return rest, -1, "" + } + return rest, i, "" + } + // An explicit @mention that matches nobody in the fight is a mistake worth + // naming — silently casting it on yourself would waste the slot. + if explicit { + return args, -1, fmt.Sprintf("**%s** isn't in this fight. Cast it on someone who is.", name) + } + return args, -1, "" +} + func (p *AdventurePlugin) castActionForSeat(ct *combatTurn, seat int, args string) (PlayerAction, func(bool), string) { noop := func(bool) {} uid := id.UserID(ct.sess.seatUserID(seat)) + // §1 — a heal may name somebody else in the fight: `!cast cure wounds @alex`. + // Split the target off before the spell is parsed, so the spell parser sees + // the same string it always has. + args, targetSeat, targetErr := splitCastTarget(ct, seat, args) + if targetErr != "" { + return PlayerAction{}, noop, targetErr + } + advChar, _ := loadAdvCharacter(uid) c, err := p.ensureCharForDnDCmd(uid, advChar) if err != nil || c == nil { @@ -612,6 +693,20 @@ func (p *AdventurePlugin) castActionForSeat(ct *combatTurn, seat int, args strin PlayerHeal: out.PlayerHeal, EnemySkip: out.EnemySkip, } + // §1 — redirect the heal onto the named ally. The roll is the same; only + // the body it lands on changes. This is the line that makes a cleric a + // cleric: until it existed, every heal in the engine was a self-heal, and + // the class whose whole job is keeping other people upright could not put + // a single hit point on a friend. + if targetSeat >= 0 && targetSeat != seat { + if out.PlayerHeal <= 0 { + return PlayerAction{}, refund, fmt.Sprintf( + "%s isn't something you can cast on someone else. Drop the target to cast it yourself.", spell.Name) + } + eff.AllyHeal, eff.AllySeat = out.PlayerHeal, targetSeat + eff.PlayerHeal = 0 + eff.Label = fmt.Sprintf("%s → %s (+%d HP)", spell.Name, ct.players[targetSeat].Name, out.PlayerHeal) + } // Concentration AOE damage spells linger: the burst lands this round // (EnemyDamage) and the same value re-ticks every round_end after, via // the engine's concentration aura. spiritual_weapon already covers the diff --git a/internal/plugin/combat_engine.go b/internal/plugin/combat_engine.go index 340ee07..ad6b285 100644 --- a/internal/plugin/combat_engine.go +++ b/internal/plugin/combat_engine.go @@ -220,7 +220,14 @@ type CombatEvent struct { // // It exists so a party's play-by-play can name the right person. Solo events // are all seat 0, and the omitempty tag keeps the field out of every solo - // turn_log_json — rows written before N3/P5 decode unchanged. + // turn_log_json — rows written before N3/P5 decode unchanged, and a fight in + // flight across a deploy resumes byte-identically (TestP5Fields_StayOffSoloRows). + // + // The omitempty makes seat 0 and "no seat" identical on the wire, which is + // fine for persistence and actively misleading in a diagnostic trace — it hid + // a companion who never swung, making the fight look like it had one seat. + // Do NOT fix that here; the wire format is load-bearing. The sim's trace + // serializes through simTraceEvent, which always emits the seat. Seat int `json:"Seat,omitempty"` } diff --git a/internal/plugin/combat_engine_party.go b/internal/plugin/combat_engine_party.go index 45a4930..19f4e7d 100644 --- a/internal/plugin/combat_engine_party.go +++ b/internal/plugin/combat_engine_party.go @@ -307,8 +307,20 @@ func roundInitiative(st *combatState, enemy *Combatant, phase *CombatPhase) []in // between — a per-round coin-flip for the extra action is the only way to land // the band in the gap. The single draw is taken only for a party, so the solo RNG // stream is untouched. +// §2(b): the budget counts the seats still STANDING, re-derived every round — +// not the seats that walked in. +// +// It used to read len(st.actors), which includes the dead. So a party that lost a +// member kept paying for them: the survivor faced a boss still swinging at +// two-player cadence, alone. That is a death spiral with the arrow pointing the +// wrong way — the moment a party is losing, the engine hits it harder — and it is +// the single nastiest thing the companion sweep turned up, because it is not a +// companion bug at all. It has been live for every human party since N3. +// +// A corpse does not threaten anybody, and the enemy has no reason to keep spending +// actions on one. func enemyActionsThisRound(st *combatState) int { - n := len(st.actors) + n := livingActors(st) if n < 2 { return 1 } @@ -320,6 +332,17 @@ func enemyActionsThisRound(st *combatState) int { return base } +// livingActors counts the seats still standing. +func livingActors(st *combatState) int { + n := 0 + for _, a := range st.actors { + if a.playerHP > 0 { + n++ + } + } + return n +} + // partyActionExpectation is the expected number of enemy attack-actions per round // against a party of n. A single enemy swing (the pre-P8 behaviour) let each // member absorb ~1/N² of the solo incoming and cleared 100% of T5; the sim band diff --git a/internal/plugin/combat_party_characterization_test.go b/internal/plugin/combat_party_characterization_test.go new file mode 100644 index 0000000..43614c9 --- /dev/null +++ b/internal/plugin/combat_party_characterization_test.go @@ -0,0 +1,159 @@ +package plugin + +import ( + "flag" + "fmt" + "math/rand/v2" + "os" + "path/filepath" + "strings" + "testing" +) + +// Party characterization — the net that did not exist. +// +// TestCombatCharacterization pins SOLO combat exhaustively, and it has been the +// tripwire for every balance change since. Party combat had nothing. The whole +// N-body layer — initiative order, per-seat resolution, enemy targeting, the P8 +// action-economy and HP scaling, downed-seat handling — shipped unpinned. +// +// What that cost: N3 shipped a Cleric class that cannot heal a single ally (no +// action in the engine can target another seat) and not one test went red. The +// hired companion then stood in fights doing nothing, and the unit tests stayed +// green through that too. It took a 1500-run expedition sweep to see either. +// +// So: pin it. Any change to the party engine now has to state, in the diff, what +// it moved. Regenerate ONLY on purpose: +// +// go test ./internal/plugin -run TestPartyCharacterization -update +// +// This golden covers simulateParty — the auto-resolve engine, which decides most +// expedition rooms and which the balance harness is built on. The turn engine's +// party half (manual play + boss fights) needs DB fixtures and is pinned by +// combat_turn_party_test.go; widening THAT into a golden is the follow-up. + +var updatePartyGolden = flag.Bool("update-party", false, "rewrite the party characterization golden") + +// partyScenario is one pinned N-body fight. +type partyScenario struct { + name string + seats []Combatant + enemy Combatant + phases []CombatPhase +} + +// seatOfClass shapes a seat that stands in for a class archetype. These are +// deliberately engine-level (stat blocks, not sheets): the golden pins what the +// ENGINE does with a roster, not what the character layer feeds it. +func seatOfClass(name string, hp, atk, def, speed int) Combatant { + c := basePlayer() + c.Name = name + c.Stats.MaxHP = hp + c.Stats.Attack = atk + c.Stats.Defense = def + c.Stats.Speed = speed + return c +} + +func partyCharacterizationScenarios() []partyScenario { + tank := seatOfClass("Tank", 160, 14, 12, 8) + striker := seatOfClass("Striker", 90, 22, 6, 14) + support := seatOfClass("Support", 110, 11, 9, 10) + // The seat this whole plan is about: a body that is real but below median. + // If scaling ever stops overcharging for it, THIS line is what moves. + weak := seatOfClass("Weak", 70, 8, 5, 9) + // A seat that will fall early. Its corpse must not keep buffing the enemy — + // when §2 lands, this scenario is the one that proves it. + glass := seatOfClass("Glass", 12, 18, 0, 16) + + return []partyScenario{ + {"duo/even", []Combatant{tank, striker}, tankyEnemy(), dungeonCombatPhases}, + {"duo/tank+support", []Combatant{tank, support}, tankyEnemy(), dungeonCombatPhases}, + {"duo/median+weak", []Combatant{tank, weak}, tankyEnemy(), dungeonCombatPhases}, + {"duo/glass falls early", []Combatant{tank, glass}, hardHitEnemy(), dungeonCombatPhases}, + {"trio/even", []Combatant{tank, striker, support}, tankyEnemy(), dungeonCombatPhases}, + {"trio/one weak seat", []Combatant{tank, striker, weak}, tankyEnemy(), dungeonCombatPhases}, + {"trio/two glass seats", []Combatant{tank, glass, glass}, hardHitEnemy(), dungeonCombatPhases}, + {"duo/vs ability enemy", []Combatant{tank, striker}, + abilityEnemy("Wither", "poison", "Duel"), dungeonCombatPhases}, + // The degenerate case. A one-seat roster MUST stay bit-identical to solo — + // it is the invariant the entire balance corpus rests on, and the reason + // the solo golden is allowed to stay untouched while this file grows. + {"solo/one-seat roster", []Combatant{basePlayer()}, baseEnemy(), dungeonCombatPhases}, + } +} + +// formatPartyResult prints the seat on EVERY line. The solo formatter does not +// (it cannot; there is only one seat), and CombatEvent.Seat is `omitempty`, so a +// JSON trace renders seat 0 and "no seat" identically — which sent me chasing a +// phantom "the companion never swings" bug for an hour. A party golden that +// could not tell you WHO acted would be worth very little. +func formatPartyResult(r PartyCombatResult) string { + var b strings.Builder + fmt.Fprintf(&b, "result: won=%v timedOut=%v rounds=%d survivors=%v\n", + r.PlayerWon, r.TimedOut, r.TotalRounds, r.AnySurvivor()) + fmt.Fprintf(&b, " enemy: start=%d entry=%d end=%d\n", r.EnemyStartHP, r.EnemyEntryHP, r.EnemyEndHP) + for i, s := range r.Seats { + fmt.Fprintf(&b, " seat[%d]: start=%d entry=%d end=%d\n", + i, s.PlayerStartHP, s.PlayerEntryHP, s.PlayerEndHP) + } + for i, e := range r.Events { + fmt.Fprintf(&b, " [%02d] r%d seat=%d %-12s %-8s %-16s dmg=%-5d php=%-4d ehp=%-4d roll=%d/%d desc=%q\n", + i, e.Round, e.Seat, e.Phase, e.Actor, e.Action, e.Damage, e.PlayerHP, e.EnemyHP, + e.Roll, e.RollAgainst, e.Desc) + } + return b.String() +} + +func TestPartyCharacterization(t *testing.T) { + var b strings.Builder + for _, sc := range partyCharacterizationScenarios() { + for _, seed := range charSeeds { + rng := rand.New(rand.NewPCG(seed, 0xC0FFEE)) + res := simulatePartyWithRNG(sc.seats, sc.enemy, sc.phases, rng) + fmt.Fprintf(&b, "=== %s seed=%d ===\n", sc.name, seed) + b.WriteString(formatPartyResult(res)) + b.WriteString("\n") + } + } + got := b.String() + + path := filepath.Join("testdata", "party_characterization.golden") + if *updatePartyGolden { + if err := os.WriteFile(path, []byte(got), 0o644); err != nil { + t.Fatal(err) + } + t.Log("party golden rewritten:", path) + return + } + want, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read party golden (run with -update-party to create it): %v", err) + } + if string(want) != got { + t.Fatalf("PARTY ENGINE BEHAVIOUR MOVED.\n\n%s\n\n"+ + "If this was deliberate, say so in the commit and regenerate:\n"+ + " go test ./internal/plugin -run TestPartyCharacterization -update-party", + firstDiff(string(want), got)) + } +} + +// The one-seat roster is the solo engine. If this ever fails, the N-body path has +// stopped being a superset of the path the entire balance corpus was measured on, +// and every baseline in the repo is suspect. +func TestPartyCharacterization_OneSeatIsStillSolo(t *testing.T) { + for _, seed := range charSeeds { + solo := simulateCombatWithRNG(basePlayer(), baseEnemy(), dungeonCombatPhases, + rand.New(rand.NewPCG(seed, 0xC0FFEE))) + party := simulatePartyWithRNG([]Combatant{basePlayer()}, baseEnemy(), dungeonCombatPhases, + rand.New(rand.NewPCG(seed, 0xC0FFEE))) + + if solo.PlayerWon != party.PlayerWon || solo.TotalRounds != party.TotalRounds || + solo.EnemyEndHP != party.EnemyEndHP || solo.PlayerEndHP != party.Seats[0].PlayerEndHP { + t.Fatalf("seed %d: a one-seat roster diverged from solo\n solo: won=%v rounds=%d ehp=%d php=%d\n party: won=%v rounds=%d ehp=%d php=%d", + seed, + solo.PlayerWon, solo.TotalRounds, solo.EnemyEndHP, solo.PlayerEndHP, + party.PlayerWon, party.TotalRounds, party.EnemyEndHP, party.Seats[0].PlayerEndHP) + } + } +} diff --git a/internal/plugin/combat_party_finish.go b/internal/plugin/combat_party_finish.go index b491bae..2e6ed97 100644 --- a/internal/plugin/combat_party_finish.go +++ b/internal/plugin/combat_party_finish.go @@ -55,6 +55,14 @@ func (p *AdventurePlugin) finishPartyCombatSession(ct *combatTurn) []string { // so does the bookkeeping that outlives the fight — a Berserker who raged and // lost is still exhausted. Both fan out; neither is the owner's alone. for seat := range sess.RosterSize() { + // The companion has no sheet to persist HP onto and no bookkeeping that + // outlives the fight — he arrives fresh next time. Skipping him here is + // not just tidiness: postCombatBookkeepingForSeat logs at ERROR when a + // seat has no sheet, so leaving him in would file an error for every + // party fight he is ever hired for. + if isCompanionUser(sess.seatUserID(seat)) { + continue + } persistDnDHPAfterCombat(id.UserID(sess.seatUserID(seat)), sess.seatHP(seat)) p.postCombatBookkeepingForSeat(sess, seat) } @@ -100,6 +108,16 @@ func (p *AdventurePlugin) finishPartyWin( uid := id.UserID(sess.seatUserID(seat)) hp, hpMax := sess.seatHP(seat), sess.seatHPMax(seat) + // The hired companion takes no cut. He earns no XP (there is no sheet to + // put it on), rolls no loot (dropZoneLoot writes real inventory rows for + // whatever id it is handed, and a bot with a magic sword is nobody's + // intent), and takes no death row. His seat renders nothing: he is not + // reading this. He was paid up front. + if isCompanionSeat(uid) { + out[seat] = "" + continue + } + // A member who went down before the killing blow still earns the kill. p.grantSeatWinXP(uid, hp, hpMax, monster, tier) @@ -148,7 +166,17 @@ func (p *AdventurePlugin) finishPartyLoss(ct *combatTurn, zone ZoneDefinition, c line := twinBeeLine(zone.ID, DMPlayerDeath, sess.RunID, cadence) out := make([]string, sess.RosterSize()) for seat := range sess.RosterSize() { - markAdventureDead(id.UserID(sess.seatUserID(seat)), "zone", zone.Display) + uid := id.UserID(sess.seatUserID(seat)) + // The companion does not die. markAdventureDead is a silent no-op for him + // today (no player_meta row to mark), but relying on that accident is how + // he ends up in the graveyard the first time someone gives him a row — + // and emitDeathNews would have the news bot file a death notice about + // itself. Say it out loud instead. + if isCompanionSeat(uid) { + out[seat] = "" + continue + } + markAdventureDead(uid, "zone", zone.Display) var b strings.Builder if line != "" && seat == 0 { b.WriteString(line + "\n") diff --git a/internal/plugin/combat_party_start.go b/internal/plugin/combat_party_start.go index c2f715a..750ced8 100644 --- a/internal/plugin/combat_party_start.go +++ b/internal/plugin/combat_party_start.go @@ -33,7 +33,9 @@ func fightRoster(sender id.UserID) []id.UserID { if err != nil || e == nil { return []id.UserID{sender} } - return expeditionAudience(e) + // Seats, not audience: the hired companion fights even though he never + // receives a DM about it. + return expeditionSeats(e) } // buildFightSeats turns a roster into the seats that will actually sit down, and @@ -59,6 +61,25 @@ func (p *AdventurePlugin) buildFightSeats( for i, uid := range roster { leader := i == 0 + // The hired companion. He must be handled before everything below: + // dndHPSnapshot returns (0,0) for a user with no sheet, so the very next + // check would quietly sit him out of every fight he was paid for, and + // buildZoneCombatants would then fail on him anyway. + // + // He is latched onto autopilot at seat time rather than after the away-player + // deadline — nobody is going to type for him, and waiting three minutes to + // discover that would stall the fight and then announce him to the party as + // an absent player. + if isCompanionSeat(uid) { + class, level := companionLoadout(companionExpeditionFor(roster[0])) + player, _, _ := p.companionCombatant(class, level, monster, tier, dmMood) + seats = append(seats, CombatSeatSetup{ + UserID: uid, HP: player.Stats.MaxHP, HPMax: player.Stats.MaxHP, + Mods: player.Mods, C: &player, EngineDriven: true, + }) + continue + } + // Both refusals below are cheap and neither needs the build, so they run // before it: consuming a seat's armed ability and *then* sitting them out // would spend their rage on a fight they never joined. @@ -159,6 +180,12 @@ func (p *AdventurePlugin) announcePartyFightStart( names[i] = c.Name } for seat, uid := range sess.SeatUserIDs() { + // Seat-keyed fan-out, so it bypasses expeditionAudience's filter — the + // companion sits down but is never written to. (He also has no magic items + // to line up, and activeMagicItemsLine would go looking for them.) + if isCompanionUser(uid) { + continue + } var b strings.Builder b.WriteString(header) b.WriteString(fmt.Sprintf("You: **%d/%d HP**.\n", sess.seatHP(seat), sess.seatHPMax(seat))) diff --git a/internal/plugin/combat_party_turn.go b/internal/plugin/combat_party_turn.go index da5c7d6..cc0ec17 100644 --- a/internal/plugin/combat_party_turn.go +++ b/internal/plugin/combat_party_turn.go @@ -166,6 +166,15 @@ func (p *AdventurePlugin) beginCombatTurn(sender id.UserID, noFightMsg string) ( return fail(notYourTurnMsg(players, acting, waiting)) } + // An engine-driven seat has nobody to hand the wheel back to. A command + // arriving from one is not a player returning to the keyboard — it is a driver + // impersonating a seat, which is precisely the confusion that used to strand + // the companion: his own auto-played turn came back through here looking like + // a keystroke and cleared the latch that was moving him. Refuse it outright. + if sess.seatIsEngineDriven(seat) { + return fail("That seat isn't yours to play.") + } + // They typed, so they are here. Hand back the wheel. sess.actorStatusesPtr(seat).Autopilot = false @@ -315,6 +324,33 @@ func (p *AdventurePlugin) nudgeStalledPartyTurn(sessionID string) { p.announcePartyRound(ct, events, preamble, p.closePartyRound(ct)) } +// driveEngineSeat plays one engine-driven seat's turn and persists the result. +// It is the seat-driver equivalent of a player typing `!attack`, for a seat that +// will never type anything. +// +// It exists because the alternative — dispatching a combat command as that seat — +// sends the turn back through beginCombatTurn, which reads any command from a +// seat as "that player is back". For a human that is correct. For a seat with no +// human it is fatal: it drops the latch that is the only thing moving them. +func (p *AdventurePlugin) driveEngineSeat(sess *CombatSession, seat int) error { + players, enemy, err := p.partyCombatantsForSession(sess) + if err != nil { + return fmt.Errorf("rebuild for engine seat: %w", err) + } + ct := &combatTurn{ + sess: sess, players: players, enemy: enemy, + seat: seat, uid: id.UserID(sess.seatUserID(seat)), + } + if _, err := p.runAutoSeatTurn(ct, seat); err != nil { + return err + } + if err := saveCombatSession(sess); err != nil { + return fmt.Errorf("save after engine seat turn: %w", err) + } + p.closePartyRound(ct) + return nil +} + // turnDeadlineLapsed reports whether the fight has sat on one member's turn past // partyTurnDeadline. LastActionAt is stamped by every saveCombatSession, and the // save that parked the fight on this seat's player_turn was the last one — so it @@ -387,6 +423,12 @@ func (p *AdventurePlugin) announcePartyRound(ct *combatTurn, events []CombatEven names := ct.seatNames() acting, waiting := actingSeat(ct.sess, ct.players, ct.enemy) for seat, uid := range ct.sess.SeatUserIDs() { + // The combat fan-out is seat-keyed, so it does not pass through + // expeditionAudience and does not inherit its companion filter. He fights; + // he is not written to about it. + if isCompanionUser(uid) { + continue + } // Rendered once per reader: the flavor pool speaks in the second person, // so each member's own events must be theirs and nobody else's. body := RenderPartyTurnRound(events, names, ct.enemy.Name, seat) diff --git a/internal/plugin/combat_session.go b/internal/plugin/combat_session.go index 827ab29..49b13b3 100644 --- a/internal/plugin/combat_session.go +++ b/internal/plugin/combat_session.go @@ -121,7 +121,7 @@ type ActorStatuses struct { // fight, set when its turn deadline lapses once (see partyTurnDeadline). // Without the latch an away member taxes the party the full deadline every // single round; with it, they cost one wait and then resolve instantly. Any - // combat command from that member clears it. + // combat command from that member clears it — they typed, so they are back. // // Like the Buff* deltas it is session-layer state with no combatState // counterpart, so snapshotActor carries it over from the prior snapshot @@ -129,6 +129,24 @@ type ActorStatuses struct { // fight has no turn deadline, only the 1h session reaper. Autopilot bool `json:"autopilot,omitempty"` + // EngineDriven marks a seat that has NO human behind it and never will: a + // hired companion today, an NPC ally or a Pete-led expedition tomorrow. It is + // the answer to "who owns this turn", and it is deliberately NOT the same + // question as Autopilot. + // + // Autopilot means "a human is away"; it is provisional, and a keystroke ends + // it. EngineDriven means "there is nobody to come back", and nothing clears it. + // Collapsing the two is a bug with teeth: the expedition autopilot drives a + // party by dispatching each seat's turn as a command from that seat, so an + // engine seat's own auto-played move arrived back at beginCombatTurn looking + // exactly like a player returning to the keyboard, and cleared the very latch + // that was moving it. The seat then stood in the fight doing nothing for the + // rest of the fight — while the enemy it had inflated by 15% killed the party. + // + // So the property lives on the seat, not on an identity check, and no command + // path can unset it. + EngineDriven bool `json:"engine_driven,omitempty"` + // Debuffs the enemy has stacked onto this character specifically. PlayerAtkDrain int `json:"player_atk_drain,omitempty"` PlayerACDebuff int `json:"player_ac_debuff,omitempty"` @@ -256,7 +274,8 @@ func seedActorOneShots(st *ActorStatuses, seat CombatSeatSetup) bool { st.ArcaneWardHP = playerMods.ArcaneWardHP st.HealChargesLeft = playerMods.HealItemCharges st.ArmedAbility = seat.ArmedAbility - return st.WardCharges != 0 || st.SporeRounds != 0 || st.ReflectFrac != 0 || + st.EngineDriven = seat.EngineDriven + return st.EngineDriven || st.WardCharges != 0 || st.SporeRounds != 0 || st.ReflectFrac != 0 || st.AutoCritFirst || st.ArcaneWardHP != 0 || st.HealChargesLeft != 0 || st.ArmedAbility != "" } @@ -387,16 +406,30 @@ func (s *CombatSession) seatOf(userID id.UserID) (int, bool) { // turn silently rather than blocking the round on a corpse. func (s *CombatSession) seatAlive(seat int) bool { return s.seatHP(seat) > 0 } -// seatIsAutopiloted reports whether a seat has been latched onto the auto-picker -// by a lapsed turn deadline. +// seatIsEngineDriven reports whether a seat has no human behind it at all — a +// hired companion, an NPC ally. Permanent for the life of the fight; no command +// clears it. Contrast seatIsAutopiloted, which is a human who stepped away. +func (s *CombatSession) seatIsEngineDriven(seat int) bool { + return s.actorStatusesForSeat(seat).EngineDriven +} + +// seatIsAutopiloted reports whether the engine, rather than a person, decides +// this seat's action — because its human is away (a lapsed turn deadline) or +// because it never had one. +// +// Every driver in the codebase reads this, which is exactly why the two reasons +// share one accessor: the picker does not care WHY nobody is typing. What differs +// is who is allowed to take the wheel back, and that question is asked in +// beginCombatTurn against seatIsEngineDriven — never here. func (s *CombatSession) seatIsAutopiloted(seat int) bool { - return s.actorStatusesForSeat(seat).Autopilot + st := s.actorStatusesForSeat(seat) + return st.Autopilot || st.EngineDriven } // seatNeedsNoHuman reports whether the engine can resolve a seat's turn without -// waiting on its player: it is down (forfeits silently) or latched onto -// autopilot. driveCombatRound keeps stepping while this holds, so a round only -// comes to rest on a live human's turn. +// waiting on its player: it is down (forfeits silently), latched onto autopilot, +// or has no human to wait for. driveCombatRound keeps stepping while this holds, +// so a round only comes to rest on a live human's turn. func (s *CombatSession) seatNeedsNoHuman(seat int) bool { return !s.seatAlive(seat) || s.seatIsAutopiloted(seat) } @@ -553,6 +586,12 @@ type CombatSeatSetup struct { // if they armed nothing. It is persisted onto the seat's statuses so every // later rebuild can re-apply the ability without re-spending it. ArmedAbility string + // EngineDriven seats this combatant with no human behind it — the hired + // companion today. It resolves from the opening round rather than after the + // 3-minute away-player deadline (nobody is coming, so waiting out a deadline + // would idle the fight and then announce him to the party as absent), and no + // command can hand the wheel back to a player who does not exist. + EngineDriven bool } // getActiveCombatSession returns the player's in-flight fight, or (nil, nil). diff --git a/internal/plugin/combat_session_build.go b/internal/plugin/combat_session_build.go index d23d3e8..38f8a88 100644 --- a/internal/plugin/combat_session_build.go +++ b/internal/plugin/combat_session_build.go @@ -160,6 +160,29 @@ func (p *AdventurePlugin) partyCombatantsForSession(sess *CombatSession) ([]*Com // id parked on their statuses. Re-applying it here — not re-consuming — // is what makes a rage last the whole fight instead of one round. st := sess.actorStatusesForSeat(seat) + + // The hired companion has no sheet to load — by design, because a + // player_meta row for him is the thing that would turn him into a real + // character everywhere. Synthesize his seat instead. This branch is + // load-bearing: buildZoneCombatants would fail on him, and one + // unbuildable seat fails the whole rebuild, which is what every human's + // !attack in the fight depends on. + if isCompanionUser(uid) { + class, level := companionLoadoutForRun(sess.RunID) + player, en, _ := p.companionCombatant(class, level, monster, int(zone.Tier), run.DMMood) + applySessionBuffs(&player, st) + players[seat] = &player + if seat == 0 { + // Unreachable today (seat 0 is always the leader), but if it ever + // isn't, the enemy still has to come from somewhere. + enemy = en + if sess.IsParty() { + enemy.Stats.MaxHP = scaledEnemyMaxHP(enemy.Stats.MaxHP, sess.RosterSize()) + } + } + continue + } + player, e, _, err := p.buildZoneCombatants(id.UserID(uid), monster, int(zone.Tier), run.DMMood, st.ArmedAbility) if err != nil { return nil, nil, fmt.Errorf("seat %d (%s): %w", seat, uid, err) diff --git a/internal/plugin/combat_turn_engine.go b/internal/plugin/combat_turn_engine.go index e5ade57..6071e1f 100644 --- a/internal/plugin/combat_turn_engine.go +++ b/internal/plugin/combat_turn_engine.go @@ -65,6 +65,19 @@ type turnActionEffect struct { EnemyDamage int PlayerHeal int EnemySkip bool // control spell: enemy forfeits its attack this round + + // AllyHeal heals ANOTHER seat instead of the caster — the thing the engine + // could not do until §1, and the reason a party cleric was a cleric in name + // only. Every heal in the engine wrote to the acting seat: MistyHealProc, + // HealItem, PlayerHeal above. Nothing could put a hit point on a friend, so + // the class whose entire identity is keeping other people alive could not. + // N3 shipped that way and no test noticed, because there was no party golden. + // + // Zero means no ally heal, which keeps every existing construction of this + // struct meaning exactly what it meant before. AllySeat is only read when + // AllyHeal > 0, so its zero value is never mistaken for "seat 0". + AllyHeal int + AllySeat int // ConcentrationDmg arms a per-round aura tick when a concentration damage // spell is cast: EnemyDamage is the burst that lands this round, this is // what re-ticks at every round_end after. Zero for one-shot spells; a @@ -560,6 +573,25 @@ func (te *turnEngine) stepPlayerActionEffect(eff *turnActionEffect) { hpCap := max(1, st.hpMax-st.maxHPDrain) st.playerHP = min(hpCap, st.playerHP+eff.PlayerHeal) } + // §1 — heal somebody else. The caster's cursor stays where it is; only the + // target's HP moves. + // + // A downed seat is NOT raised. Death in this engine is terminal for the fight + // (the close-out marks them, the hospital takes them), and a heal that + // resurrected a corpse would quietly rewrite the loss rules every close-out + // path depends on. Healing keeps people up; it does not bring them back. + if eff.AllyHeal > 0 && eff.AllySeat >= 0 && eff.AllySeat < len(st.actors) { + if tgt := st.actors[eff.AllySeat]; tgt.playerHP > 0 { + cap := max(1, tgt.hpMax-tgt.maxHPDrain) + before := tgt.playerHP + tgt.playerHP = min(cap, tgt.playerHP+eff.AllyHeal) + st.events = append(st.events, CombatEvent{ + Round: st.round, Phase: turnCombatPhase.Name, Actor: "player", Action: "ally_heal", + Damage: tgt.playerHP - before, PlayerHP: tgt.playerHP, EnemyHP: st.enemyHP, + Seat: eff.AllySeat, Desc: eff.Label, + }) + } + } // Arm / replace the concentration aura. A new concentration cast overwrites // the old one (5e: one concentration at a time); non-concentration casts // leave any running aura alone. diff --git a/internal/plugin/dnd_expedition_cmd.go b/internal/plugin/dnd_expedition_cmd.go index ee68271..996c601 100644 --- a/internal/plugin/dnd_expedition_cmd.go +++ b/internal/plugin/dnd_expedition_cmd.go @@ -95,6 +95,10 @@ func (p *AdventurePlugin) handleDnDExpeditionCmd(ctx MessageContext, args string return p.expeditionCmdParty(ctx) case "leave": return p.expeditionCmdLeave(ctx) + case "hire": + return p.expeditionCmdHire(ctx, rest) + case "dismiss": + return p.expeditionCmdDismiss(ctx) case "extract": return p.handleExtractCmd(ctx, "") case "resume": @@ -122,7 +126,9 @@ func expeditionHelpText() string { b.WriteString("`!expedition invite @user` — they buy their own supplies into the party pool\n") b.WriteString("`!expedition accept` / `!expedition decline` — answer an invite\n") b.WriteString("`!expedition party` — who's with you\n") - b.WriteString("`!expedition leave` — turn back for town (members only)\n\n") + b.WriteString("`!expedition leave` — turn back for town (members only)\n") + b.WriteString("`!expedition hire [class]` — pay Pete to fill the role you're missing\n") + b.WriteString("`!expedition dismiss` — send Pete home\n\n") b.WriteString("**Mid-expedition:**\n") b.WriteString("`!extract` — bail safely (resumable for 7 days)\n") b.WriteString("`!resume [loadout]` — resume an extracted expedition\n") diff --git a/internal/plugin/expedition_companion_cmd.go b/internal/plugin/expedition_companion_cmd.go new file mode 100644 index 0000000..bb8f7bd --- /dev/null +++ b/internal/plugin/expedition_companion_cmd.go @@ -0,0 +1,115 @@ +package plugin + +import ( + "errors" + "fmt" + "strings" +) + +// The player-facing surface for hiring Pete. +// +// !expedition hire [class] (leader, Day 1 — auto-fills the missing role) +// !expedition dismiss (leader — sends him home, no refund) +// +// He is hired on Day 1 like any other companion, because a body that materializes +// three rooms in is not a party member, it is a cheat code. See +// adventure_companion.go for why he is an NPC seat and not a player. + +// expeditionCmdHire brings the correspondent along. +func (p *AdventurePlugin) expeditionCmdHire(ctx MessageContext, rest string) error { + exp, isLeader, err := activeExpeditionFor(ctx.Sender) + if err != nil { + return p.SendDM(ctx.Sender, "Couldn't read expedition state: "+err.Error()) + } + if exp == nil { + return p.SendDM(ctx.Sender, "No active expedition. `!expedition start ` first.") + } + if !isLeader { + return p.SendDM(ctx.Sender, "Only the party leader hires. You're along for the ride.") + } + if !inviteWindowOpen(exp) { + return p.SendDM(ctx.Sender, + "This expedition has already set off — Pete joins on Day 1 or not at all.") + } + if companionSeated(exp.ID) { + return p.SendDM(ctx.Sender, "Pete's already with you. `!expedition party` to see the roster.") + } + if p.euro == nil { + return p.SendDM(ctx.Sender, "Coin system unavailable — try again later.") + } + + zone := zoneOrFallback(exp.ZoneID) + level := companionPartyLevel(exp.ID) + + class, explicit := parseCompanionClass(rest) + if !explicit { + if arg := strings.TrimSpace(rest); arg != "" && !strings.EqualFold(arg, "auto") { + return p.SendDM(ctx.Sender, fmt.Sprintf( + "Don't know the class %q. Name one, or just `!expedition hire` and he'll fill whatever you're missing.", arg)) + } + class = companionRoleFill(companionPartyClasses(exp.ID)) + } + + cost := float64(companionHireCost(level, zone.Tier)) + if balance := p.euro.GetBalance(ctx.Sender); balance < cost { + return p.SendDM(ctx.Sender, fmt.Sprintf( + "Pete doesn't work for free. His fee for **%s** is **%d** — you have **%.0f**.", + zone.Display, int(cost), balance)) + } + if !p.euro.Debit(ctx.Sender, cost, "expedition: hired Pete") { + return p.SendDM(ctx.Sender, "Couldn't debit Pete's fee (try again).") + } + + if err := hireCompanion(exp.ID, class, level); err != nil { + p.euro.Credit(ctx.Sender, cost, "expedition: Pete hire refund") + return p.SendDM(ctx.Sender, companionRefusalText(err)) + } + + emitCompanionHireFact(ctx.Sender, class, level, zone) + + ci, _ := classInfo(class) + filled := "You asked for a " + ci.Display + "." + if !explicit { + filled = "He's filling the hole in your roster — **" + ci.Display + "**." + } + return p.SendDM(ctx.Sender, fmt.Sprintf( + "📝 **Pete's coming with you into %s.** %s He's **level %d**, and his fee was **%d**.\n\n"+ + "He fights his own turns — you don't command him. He takes no loot and no XP; he's here for the story.\n"+ + "`!expedition dismiss` sends him home (no refund — he's already packed).", + zone.Display, filled, level, int(cost))) +} + +// expeditionCmdDismiss sends him home mid-run. +func (p *AdventurePlugin) expeditionCmdDismiss(ctx MessageContext) error { + exp, isLeader, err := activeExpeditionFor(ctx.Sender) + if err != nil { + return p.SendDM(ctx.Sender, "Couldn't read expedition state: "+err.Error()) + } + if exp == nil { + return p.SendDM(ctx.Sender, "No active expedition.") + } + if !isLeader { + return p.SendDM(ctx.Sender, "Only the party leader can dismiss him.") + } + if err := dismissCompanion(exp.ID); err != nil { + return p.SendDM(ctx.Sender, companionRefusalText(err)) + } + return p.SendDM(ctx.Sender, + "📝 **Pete heads back.** He got what he came for. The fee stays spent.") +} + +// companionRefusalText turns the companion layer's sentinel errors into copy. +func companionRefusalText(err error) string { + switch { + case errors.Is(err, ErrCompanionAlreadyHired): + return "Pete's already with you." + case errors.Is(err, ErrCompanionOnAssignment): + return "Pete's out on assignment with another party. He'll be back." + case errors.Is(err, ErrCompanionNotHired): + return "Pete isn't with you." + case errors.Is(err, ErrPartyFull): + return "No room — your party's full. Someone has to `!expedition leave` first." + default: + return "Couldn't hire Pete: " + err.Error() + } +} diff --git a/internal/plugin/expedition_party.go b/internal/plugin/expedition_party.go index be5ef2f..af576cb 100644 --- a/internal/plugin/expedition_party.go +++ b/internal/plugin/expedition_party.go @@ -82,30 +82,126 @@ func partyMembers(expeditionID string) ([]PartyMember, error) { return out, rows.Err() } -// partyMemberIDs is partyMembers reduced to user ids, leader first. It is what -// the fan-out seams (digest, briefing, recap) want: a solo expedition yields -// just the owner, so a caller can loop unconditionally. -func partyMemberIDs(expeditionID, ownerID string) ([]id.UserID, error) { - members, err := partyMembers(expeditionID) +// PartySeatKind is what a seat *is*. The three answers differ in ways that matter +// at almost every seam, and conflating any two of them has already cost us a bug: +// a companion is a seat but not a mouth and not a mailbox; a leader owns the +// expedition row that everyone else references. +type PartySeatKind int + +const ( + SeatLeader PartySeatKind = iota + SeatMember + SeatCompanion +) + +// PartySeat is one body on an expedition. +type PartySeat struct { + UserID id.UserID + Kind PartySeatKind +} + +// IsHuman reports whether there is a person behind this seat — someone who can be +// DM'd, can earn loot, eats supplies, and can die. +func (s PartySeat) IsHuman() bool { return s.Kind != SeatCompanion } + +// expeditionParty is THE answer to "who is on this expedition". Every other view +// — who gets mail, who sits down in a fight, who eats — is derived from it. +// +// It ALWAYS includes the owner. That is the whole point, and it is not a +// convenience: a solo expedition has **no expedition_party rows at all** (the +// roster only materializes on the first invite — see partyMembers), so any code +// that answers "who is in this party?" by reading the roster table gets *nobody* +// for a solo player, and then quietly falls back to whatever looked sensible at +// the call site. +// +// That is not hypothetical. It is exactly how the hired companion came out at +// **level 1** for every solo player — the one player the feature exists for. The +// level was averaged over the party; the party read as empty; the fallback was 1. +// He then walked into a tier-4 zone as a level-1 body, died on contact, and left +// the leader fighting a boss that had been inflated on his account. A 1500-run +// sweep is what found it, because nothing else could. +// +// So: there is no way to ask this function for the party and be handed an empty +// list. If the expedition exists, it has at least a leader. +func expeditionParty(expeditionID, ownerID string) ([]PartySeat, error) { + rows, err := partyMembers(expeditionID) if err != nil { return nil, err } - if len(members) == 0 { - return []id.UserID{id.UserID(ownerID)}, nil + if len(rows) == 0 { + // Solo: no roster rows. The owner IS the party. + if ownerID == "" { + return nil, nil + } + return []PartySeat{{UserID: id.UserID(ownerID), Kind: SeatLeader}}, nil } - out := make([]id.UserID, 0, len(members)) - for _, m := range members { - out = append(out, id.UserID(m.UserID)) + out := make([]PartySeat, 0, len(rows)) + for _, m := range rows { + kind := SeatMember + switch { + case isCompanionUser(m.UserID): + kind = SeatCompanion + case m.IsLeader(): + kind = SeatLeader + } + out = append(out, PartySeat{UserID: id.UserID(m.UserID), Kind: kind}) } return out, nil } -// partySize is the number of seated players: 1 for a solo expedition. +// partyHumans is the party minus the companion: everyone with a person behind +// them. This is the set that gets mail, earns loot, eats supplies, and can die. +func partyHumans(expeditionID, ownerID string) ([]PartySeat, error) { + seats, err := expeditionParty(expeditionID, ownerID) + if err != nil { + return nil, err + } + out := seats[:0] + for _, s := range seats { + if s.IsHuman() { + out = append(out, s) + } + } + return out, nil +} + +// partyMemberIDs is the whole party as ids, leader first — every body, companion +// included. Callers that want only people want partyHumans. +func partyMemberIDs(expeditionID, ownerID string) ([]id.UserID, error) { + seats, err := expeditionParty(expeditionID, ownerID) + if err != nil { + return nil, err + } + out := make([]id.UserID, 0, len(seats)) + for _, s := range seats { + out = append(out, s.UserID) + } + return out, nil +} + +// partySize is the number of seated *players*: 1 for a solo expedition. +// +// A hired companion is not counted, and both of this function's consumers want +// it that way: +// +// - expeditionBurnRatePct scales the daily supply burn by party size. Pete +// never bought a pack — members buy their own on !expedition accept, and he +// accepts nothing — so counting him would bill the leader a 60% higher burn +// for a mouth that brought its own rations. He is on expenses. +// - the "your party is still waiting on you" gate blocks a leader from +// starting a new run while an extracted party is pending. A roster holding +// nobody but Pete is not a party waiting on anyone, and counting him would +// lock the leader out of the game until they abandoned the run. +// +// The combat roster is a different question with a different answer: Pete IS a +// body in the fight, so CombatSession.RosterSize() counts him and the enemy-HP +// scalar feels him. Seats and mouths are not the same set. func partySize(expeditionID string) (int, error) { var n int err := db.Get().QueryRow( - `SELECT COUNT(*) FROM expedition_party WHERE expedition_id = ?`, - expeditionID).Scan(&n) + `SELECT COUNT(*) FROM expedition_party + WHERE expedition_id = ? AND user_id <> ?`, + expeditionID, string(companionUserID())).Scan(&n) if err != nil { return 0, err } diff --git a/internal/plugin/expedition_party_cmd.go b/internal/plugin/expedition_party_cmd.go index 01ee72b..4cdab54 100644 --- a/internal/plugin/expedition_party_cmd.go +++ b/internal/plugin/expedition_party_cmd.go @@ -246,6 +246,10 @@ func (p *AdventurePlugin) expeditionCmdParty(ctx MessageContext) error { b.WriteString(fmt.Sprintf("**%s** — alone.\n", p.DisplayName(id.UserID(exp.UserID)))) } for _, m := range members { + if isCompanionUser(m.UserID) { + b.WriteString(companionRosterLine(exp.ID)) + continue + } role := "member" if m.IsLeader() { role = "leader" @@ -260,6 +264,9 @@ func (p *AdventurePlugin) expeditionCmdParty(ctx MessageContext) error { b.WriteString(fmt.Sprintf("\nSupplies: **%.1f / %.1f SU**", exp.Supplies.Current, exp.Supplies.Max)) if inviteWindowOpen(exp) { b.WriteString("\n\n_`!expedition invite @user` while it's still Day 1._") + if !companionSeated(exp.ID) { + b.WriteString("\n_Short a body? `!expedition hire` brings Pete along._") + } } return p.SendDM(ctx.Sender, b.String()) } diff --git a/internal/plugin/expedition_party_resolve.go b/internal/plugin/expedition_party_resolve.go index f2c0113..18d45a3 100644 --- a/internal/plugin/expedition_party_resolve.go +++ b/internal/plugin/expedition_party_resolve.go @@ -108,13 +108,45 @@ func seatedExpeditionFor(userID id.UserID) (*Expedition, error) { // degrades to the owner rather than dropping the message — a player who misses // their briefing because SQLite hiccuped is a worse outcome than a member who // misses theirs. +// +// A hired companion is dropped here, and this is the chokepoint that keeps him +// out of every DM seam at once: the briefing, the recap, the digest, the +// extraction notice — and, crucially, the per-member side effects that ride the +// fan-out rather than the message. maybeRollPetArrivalOnEmerge would offer Pete +// a pet and park a pending interaction awaiting a reply that never comes; +// maybeFireAnchoredEvent would claim him a daily event slot and DM him a +// choice. He is not a person; he does not get mail. See isCompanionSeat. func expeditionAudience(e *Expedition) []id.UserID { + if e == nil || e.UserID == "" { + return nil + } + seats, err := partyHumans(e.ID, e.UserID) + if err != nil { + slog.Warn("expedition: party roster read failed, DMing owner only", + "expedition", e.ID, "err", err) + return []id.UserID{id.UserID(e.UserID)} + } + out := make([]id.UserID, 0, len(seats)) + for _, s := range seats { + out = append(out, s.UserID) + } + return out +} + +// expeditionSeats is every body that sits down in a fight: the whole roster, +// hired companion included. It is deliberately NOT expeditionAudience — that one +// drops the companion because he does not get mail, and a combat roster built +// from it would seat everyone the leader paid for except the one he paid for. +// +// Mail and seats are different sets. Anything that sends is an audience; +// anything that fights is a seat. +func expeditionSeats(e *Expedition) []id.UserID { if e == nil || e.UserID == "" { return nil } ids, err := partyMemberIDs(e.ID, e.UserID) if err != nil { - slog.Warn("expedition: party roster read failed, DMing owner only", + slog.Warn("expedition: party roster read failed, seating owner only", "expedition", e.ID, "err", err) return []id.UserID{id.UserID(e.UserID)} } diff --git a/internal/plugin/expedition_sim.go b/internal/plugin/expedition_sim.go index cca3fe7..6e1d4a3 100644 --- a/internal/plugin/expedition_sim.go +++ b/internal/plugin/expedition_sim.go @@ -34,6 +34,11 @@ func simInlineBossCombat() bool { return os.Getenv("GOGOBEE_SIM_INLINE_BOSS") == type SimRunner struct { P *AdventurePlugin Euro *EuroPlugin + // Companion hires Pete into the party on Day 1: "" = none, "auto" = fill the + // missing role, or an explicit class id. He takes a combat seat and no loot, + // which is exactly the thing the sweep is here to measure — whether an extra + // below-median body lifts the trailing case without carrying it. + Companion string } // NewSimRunner initializes a fresh sqlite DB in dataDir and constructs @@ -400,7 +405,42 @@ type SimCombatSummary struct { // SetSimIncludeTrace(true) has been called, and only on the LAST // combat per expedition (the boss room) to keep JSONL size bounded. // Used by J2 caster-survival analysis. - Events []CombatEvent `json:",omitempty"` + Events []simTraceEvent `json:",omitempty"` +} + +// simTraceEvent is CombatEvent with the seat ALWAYS emitted. +// +// CombatEvent.Seat is `omitempty` for wire-compat reasons that are correct for +// persistence and wrong for a trace: seat 0 and "no seat" serialize identically, +// so a party trace silently reads as a solo fight. That is not hypothetical — it +// is what made a hired companion who never took a turn look, in the JSON, like a +// fight that only ever had one seat, and it cost an hour of chasing the wrong bug. +// A diagnostic that cannot tell you who acted is not a diagnostic. +type simTraceEvent struct { + Round int + Seat int // always present, even when 0 + Phase string + Actor string + Action string + Damage int + PlayerHP int + EnemyHP int + Roll int + RollAgainst int + Desc string `json:",omitempty"` +} + +// simTraceEvents converts a TurnLog for the trace. +func simTraceEvents(events []CombatEvent) []simTraceEvent { + out := make([]simTraceEvent, len(events)) + for i, e := range events { + out[i] = simTraceEvent{ + Round: e.Round, Seat: e.Seat, Phase: e.Phase, Actor: e.Actor, Action: e.Action, + Damage: e.Damage, PlayerHP: e.PlayerHP, EnemyHP: e.EnemyHP, + Roll: e.Roll, RollAgainst: e.RollAgainst, Desc: e.Desc, + } + } + return out } // simIncludeTrace gates per-round event capture on SimCombatSummary. @@ -516,6 +556,22 @@ func (s *SimRunner) RunPartyExpedition(uid id.UserID, members []id.UserID, zoneI return res, fmt.Errorf("expedition vanished while seating the party") } } + // Hire the companion on Day 1, after the humans are seated so the role fill + // sees the party it is filling a hole in. He is not in `members` — he buys no + // packs and takes no seat in the human roster — but fightRoster reads + // expeditionSeats, so he shows up in every fight from here on. + if s.Companion != "" { + class, ok := parseCompanionClass(s.Companion) + if !ok { + class = companionRoleFill(companionPartyClasses(exp.ID)) + } + if err := hireCompanion(exp.ID, class, companionPartyLevel(exp.ID)); err != nil { + res.Outcome = "halted" + res.StopCode = "companion:" + err.Error() + return res, err + } + } + roster := append([]id.UserID{uid}, members...) res.PartySize = len(roster) for _, m := range members { @@ -942,7 +998,7 @@ func simCombatSummaries(uid id.UserID) []SimCombatSummary { out = append(out, s) } if simIncludeTrace && len(out) > 0 { - out[len(out)-1].Events = lastEvents + out[len(out)-1].Events = simTraceEvents(lastEvents) } return out } @@ -1038,6 +1094,18 @@ func (p *AdventurePlugin) autoDriveCombat(ctx MessageContext) (bool, error) { turn.Sender = id.UserID(cur.seatUserID(seat)) } + // An engine-driven seat is never dispatched as a chat command. There is no + // character for the handlers to load, and a command arriving from that + // seat's id reads to beginCombatTurn as a player returning to the keyboard + // — which is how the driver used to clear the very latch that was moving + // the seat. Drive it directly instead. + if cur.seatIsEngineDriven(seat) { + if err := p.driveEngineSeat(cur, seat); err != nil { + return false, fmt.Errorf("engine seat %d: %w", seat, err) + } + continue + } + kind, arg := p.pickAutoCombatActionForSeat(turn.Sender, cur, seat) var dispatchErr error switch kind { @@ -1157,6 +1225,14 @@ func (p *AdventurePlugin) pickAutoCombatActionForSeat(uid id.UserID, sess *Comba } } } + // §1 — a healer with a hurt friend heals the friend. This is what a competent + // player does, and until the engine could target another seat it was not an + // option the picker even had. It runs before the damage picks: a party member + // about to die is a more urgent use of a slot than another Fireball. + if id, target := simPickAllyHeal(c, uid, sess, seat); id != "" { + return "cast", id + " @" + target + } + if isSpellcaster(c) && !simMartialFirstClass(c.Class) { // Cleric: Spiritual Weapon is a BuffSelf that fires a spectral // bonus-action attack each round via SpiritWeaponProc/Dmg mods — @@ -1261,6 +1337,85 @@ func simPickSpiritualWeapon(c *DnDCharacter, uid id.UserID, st ActorStatuses) st // - Among feasible candidates, prefer higher slot level (preserves // high-slot supremacy and burns the big slots first); tie-break on // expected damage from the dice string. +// +// simHealAllyThresholdPct is how hurt a friend has to be before a healer spends a +// slot on them rather than on damage. Deliberately lower than the self-heal +// threshold: a heal is a wasted turn if the target was going to live anyway, and +// the party's damage output is what ends the fight. +const simHealAllyThresholdPct = 45 + +// simPickAllyHeal returns the heal spell and target name a competent healer would +// use on the worst-hurt *other* seat, or ("", "") if nobody needs it, the caster +// cannot heal, or the fight is solo. +// +// This is the picker half of §1. Before the engine could target another seat there +// was nothing for it to choose, so an autopiloted or engine-driven healer simply +// swung a weapon while their friends died beside them. +func simPickAllyHeal(c *DnDCharacter, uid id.UserID, sess *CombatSession, seat int) (spellID, target string) { + if sess == nil || !sess.IsParty() || c == nil || !isSpellcaster(c) { + return "", "" + } + + // Who is worst off? Only living seats — the engine will not raise the downed, + // and a heal aimed at a corpse is a lost turn. + worst, worstPct := -1, 101 + for i := range sess.RosterSize() { + if i == seat { + continue + } + hp, hpMax := sess.seatHP(i), sess.seatHPMax(i) + if hp <= 0 || hpMax <= 0 { + continue + } + if pct := hp * 100 / hpMax; pct < worstPct { + worst, worstPct = i, pct + } + } + if worst < 0 || worstPct >= simHealAllyThresholdPct { + return "", "" + } + + // The cheapest heal that is actually castable — burning a 5th-level slot to + // top somebody up is not what a competent player does. + known, err := listKnownSpells(uid) + if err != nil { + return "", "" + } + slots, _ := getSpellSlots(uid) + best, bestLevel := "", 99 + for _, k := range known { + if !k.Prepared { + continue + } + sp, ok := lookupSpell(k.SpellID) + if !ok || sp.Effect != EffectSpellHeal || sp.CastTime == CastReaction { + continue + } + onList := false + for _, cl := range sp.Classes { + if cl == c.Class { + onList = true + break + } + } + if !onList || sp.Level == 0 { + continue // heals are slot spells; a level-0 "heal" is not a thing we cast + } + if pair, ok := slots[sp.Level]; !ok || pair[0]-pair[1] <= 0 { + continue + } + if sp.Level < bestLevel { + best, bestLevel = sp.ID, sp.Level + } + } + if best == "" { + return "", "" + } + // Target by the seat's own Matrix localpart: splitCastTarget resolves against + // the seats in this fight, so this round-trips without a room lookup. + return best, id.UserID(sess.seatUserID(worst)).Localpart() +} + func simPickSpell(c *DnDCharacter, uid id.UserID, auraActive bool) string { known, err := listKnownSpells(uid) if err != nil || len(known) == 0 { diff --git a/internal/plugin/testdata/party_characterization.golden b/internal/plugin/testdata/party_characterization.golden new file mode 100644 index 0000000..6cd8078 --- /dev/null +++ b/internal/plugin/testdata/party_characterization.golden @@ -0,0 +1,1908 @@ +=== duo/even seed=1 === +result: won=true timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=36 + seat[0]: start=160 entry=160 end=59 + seat[1]: start=90 entry=90 end=0 + [00] r1 seat=1 Opening player hit dmg=16 php=90 ehp=329 roll=5/0 desc="" + [01] r1 seat=0 Opening player hit dmg=9 php=160 ehp=320 roll=5/0 desc="" + [02] r1 seat=1 Opening enemy hit dmg=7 php=83 ehp=320 roll=11/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=6 php=77 ehp=320 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=320 roll=19/0 desc="" + [05] r1 seat=0 Opening environment environmental dmg=5 php=149 ehp=320 roll=0/0 desc="" + [06] r2 seat=1 Clash player hit dmg=16 php=77 ehp=304 roll=4/0 desc="" + [07] r2 seat=1 Clash enemy hit dmg=9 php=68 ehp=304 roll=13/0 desc="" + [08] r2 seat=0 Clash enemy hit dmg=6 php=143 ehp=304 roll=12/0 desc="" + [09] r2 seat=0 Clash player hit dmg=12 php=143 ehp=292 roll=4/0 desc="" + [10] r3 seat=0 Clash player hit dmg=10 php=143 ehp=282 roll=9/0 desc="" + [11] r3 seat=1 Clash player hit dmg=19 php=68 ehp=263 roll=6/0 desc="" + [12] r3 seat=0 Clash enemy hit dmg=8 php=135 ehp=263 roll=17/0 desc="" + [13] r3 seat=1 Clash enemy hit dmg=8 php=60 ehp=263 roll=5/0 desc="" + [14] r3 seat=0 Clash enemy hit dmg=8 php=127 ehp=263 roll=7/0 desc="" + [15] r4 seat=1 Clash player hit dmg=17 php=60 ehp=246 roll=3/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=9 php=51 ehp=246 roll=11/0 desc="" + [17] r4 seat=1 Clash enemy hit dmg=7 php=44 ehp=246 roll=8/0 desc="" + [18] r4 seat=0 Clash player hit dmg=12 php=127 ehp=234 roll=9/0 desc="" + [19] r4 seat=0 Clash environment environmental dmg=5 php=122 ehp=234 roll=0/0 desc="" + [20] r5 seat=1 Decisive player hit dmg=20 php=44 ehp=214 roll=11/0 desc="" + [21] r5 seat=0 Decisive player miss dmg=0 php=122 ehp=214 roll=1/0 desc="fumble" + [22] r5 seat=0 Decisive enemy hit dmg=9 php=113 ehp=214 roll=6/0 desc="" + [23] r5 seat=0 Decisive enemy hit dmg=7 php=106 ehp=214 roll=11/0 desc="" + [24] r6 seat=0 Sudden Death player hit dmg=14 php=106 ehp=200 roll=3/0 desc="" + [25] r6 seat=1 Sudden Death player hit dmg=23 php=44 ehp=177 roll=5/0 desc="" + [26] r6 seat=0 Sudden Death enemy crit dmg=16 php=90 ehp=177 roll=20/0 desc="" + [27] r6 seat=1 Sudden Death enemy hit dmg=8 php=36 ehp=177 roll=2/0 desc="" + [28] r6 seat=1 Sudden Death enemy block dmg=5 php=31 ehp=177 roll=19/0 desc="" + [29] r7 seat=0 Sudden Death player hit dmg=15 php=90 ehp=162 roll=8/0 desc="" + [30] r7 seat=1 Sudden Death player crit dmg=46 php=31 ehp=116 roll=20/0 desc="" + [31] r7 seat=0 Sudden Death enemy hit dmg=10 php=80 ehp=116 roll=14/0 desc="" + [32] r7 seat=1 Sudden Death enemy hit dmg=10 php=21 ehp=116 roll=10/0 desc="" + [33] r8 seat=1 Sudden Death player hit dmg=23 php=21 ehp=93 roll=12/0 desc="" + [34] r8 seat=0 Sudden Death player hit dmg=13 php=80 ehp=80 roll=7/0 desc="" + [35] r8 seat=1 Sudden Death enemy hit dmg=10 php=11 ehp=80 roll=14/0 desc="" + [36] r8 seat=0 Sudden Death enemy hit dmg=7 php=73 ehp=80 roll=8/0 desc="" + [37] r9 seat=1 Sudden Death player hit dmg=19 php=11 ehp=61 roll=7/0 desc="" + [38] r9 seat=0 Sudden Death player hit dmg=11 php=73 ehp=50 roll=18/0 desc="" + [39] r9 seat=1 Sudden Death enemy hit dmg=9 php=2 ehp=50 roll=13/0 desc="" + [40] r9 seat=1 Sudden Death enemy hit dmg=10 php=0 ehp=50 roll=13/0 desc="" + [41] r9 seat=0 Sudden Death enemy block dmg=4 php=69 ehp=50 roll=5/0 desc="" + [42] r10 seat=0 Sudden Death player hit dmg=14 php=69 ehp=36 roll=6/0 desc="" + [43] r10 seat=0 Sudden Death enemy hit dmg=10 php=59 ehp=36 roll=11/0 desc="" + [44] r10 seat=0 exhaust system timeout dmg=0 php=59 ehp=36 roll=0/0 desc="" + +=== duo/even seed=2 === +result: won=false timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=71 + seat[0]: start=160 entry=160 end=42 + seat[1]: start=90 entry=90 end=7 + [00] r1 seat=1 Opening player hit dmg=16 php=90 ehp=329 roll=12/0 desc="" + [01] r1 seat=0 Opening player hit dmg=9 php=160 ehp=320 roll=3/0 desc="" + [02] r1 seat=0 Opening enemy crit dmg=14 php=146 ehp=320 roll=20/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=7 php=83 ehp=320 roll=12/0 desc="" + [04] r2 seat=1 Clash player hit dmg=17 php=83 ehp=303 roll=5/0 desc="" + [05] r2 seat=0 Clash player hit dmg=11 php=146 ehp=292 roll=3/0 desc="" + [06] r2 seat=0 Clash enemy hit dmg=8 php=138 ehp=292 roll=18/0 desc="" + [07] r2 seat=0 Clash enemy hit dmg=8 php=130 ehp=292 roll=10/0 desc="" + [08] r3 seat=1 Clash player hit dmg=19 php=83 ehp=273 roll=16/0 desc="" + [09] r3 seat=0 Clash enemy miss dmg=0 php=130 ehp=273 roll=1/0 desc="fumble" + [10] r3 seat=0 Clash enemy hit dmg=8 php=122 ehp=273 roll=9/0 desc="" + [11] r3 seat=0 Clash player hit dmg=11 php=122 ehp=262 roll=16/0 desc="" + [12] r4 seat=1 Clash player miss dmg=0 php=83 ehp=262 roll=1/0 desc="fumble" + [13] r4 seat=0 Clash player hit dmg=10 php=122 ehp=252 roll=19/0 desc="" + [14] r4 seat=0 Clash enemy hit dmg=6 php=116 ehp=252 roll=11/0 desc="" + [15] r4 seat=1 Clash enemy hit dmg=9 php=74 ehp=252 roll=7/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=9 php=65 ehp=252 roll=4/0 desc="" + [17] r5 seat=1 Decisive player hit dmg=21 php=65 ehp=231 roll=9/0 desc="" + [18] r5 seat=1 Decisive enemy hit dmg=9 php=56 ehp=231 roll=19/0 desc="" + [19] r5 seat=0 Decisive enemy hit dmg=8 php=108 ehp=231 roll=18/0 desc="" + [20] r5 seat=0 Decisive player hit dmg=12 php=108 ehp=219 roll=8/0 desc="" + [21] r6 seat=1 Sudden Death player block dmg=9 php=56 ehp=210 roll=10/0 desc="" + [22] r6 seat=0 Sudden Death player hit dmg=14 php=108 ehp=196 roll=16/0 desc="" + [23] r6 seat=1 Sudden Death enemy block dmg=4 php=52 ehp=196 roll=4/0 desc="" + [24] r6 seat=1 Sudden Death enemy hit dmg=11 php=41 ehp=196 roll=15/0 desc="" + [25] r6 seat=1 Sudden Death enemy hit dmg=10 php=31 ehp=196 roll=8/0 desc="" + [26] r7 seat=1 Sudden Death player hit dmg=20 php=31 ehp=176 roll=8/0 desc="" + [27] r7 seat=0 Sudden Death player hit dmg=11 php=108 ehp=165 roll=6/0 desc="" + [28] r7 seat=0 Sudden Death enemy hit dmg=8 php=100 ehp=165 roll=18/0 desc="" + [29] r7 seat=1 Sudden Death enemy hit dmg=11 php=20 ehp=165 roll=6/0 desc="" + [30] r8 seat=1 Sudden Death player hit dmg=21 php=20 ehp=144 roll=11/0 desc="" + [31] r8 seat=0 Sudden Death player hit dmg=11 php=100 ehp=133 roll=17/0 desc="" + [32] r8 seat=0 Sudden Death enemy crit dmg=20 php=80 ehp=133 roll=20/0 desc="" + [33] r8 seat=1 Sudden Death enemy hit dmg=11 php=9 ehp=133 roll=10/0 desc="" + [34] r8 seat=0 Sudden Death enemy hit dmg=10 php=70 ehp=133 roll=5/0 desc="" + [35] r9 seat=0 Sudden Death player hit dmg=12 php=70 ehp=121 roll=14/0 desc="" + [36] r9 seat=1 Sudden Death player hit dmg=21 php=9 ehp=100 roll=6/0 desc="" + [37] r9 seat=0 Sudden Death enemy hit dmg=10 php=60 ehp=100 roll=13/0 desc="" + [38] r9 seat=0 Sudden Death enemy miss dmg=0 php=60 ehp=100 roll=1/0 desc="fumble" + [39] r10 seat=1 Sudden Death player hit dmg=18 php=9 ehp=82 roll=8/0 desc="" + [40] r10 seat=0 Sudden Death player hit dmg=11 php=60 ehp=71 roll=8/0 desc="" + [41] r10 seat=0 Sudden Death enemy hit dmg=8 php=52 ehp=71 roll=5/0 desc="" + [42] r10 seat=0 Sudden Death enemy hit dmg=10 php=42 ehp=71 roll=8/0 desc="" + [43] r10 seat=1 Sudden Death environment environmental dmg=2 php=7 ehp=71 roll=0/0 desc="" + [44] r10 seat=0 exhaust system timeout dmg=0 php=42 ehp=71 roll=0/0 desc="" + +=== duo/even seed=3 === +result: won=true timedOut=true rounds=9 survivors=true + enemy: start=345 entry=345 end=68 + seat[0]: start=160 entry=160 end=47 + seat[1]: start=90 entry=90 end=8 + [00] r1 seat=1 Opening player hit dmg=14 php=90 ehp=331 roll=11/0 desc="" + [01] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=331 roll=18/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=6 php=148 ehp=331 roll=10/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=8 php=82 ehp=331 roll=13/0 desc="" + [04] r1 seat=0 Opening player hit dmg=8 php=148 ehp=323 roll=5/0 desc="" + [05] r2 seat=1 Clash player hit dmg=19 php=82 ehp=304 roll=2/0 desc="" + [06] r2 seat=0 Clash player hit dmg=11 php=148 ehp=293 roll=19/0 desc="" + [07] r2 seat=0 Clash enemy hit dmg=8 php=140 ehp=293 roll=2/0 desc="" + [08] r2 seat=0 Clash enemy hit dmg=6 php=134 ehp=293 roll=6/0 desc="" + [09] r2 seat=1 Clash environment environmental dmg=6 php=76 ehp=293 roll=0/0 desc="" + [10] r3 seat=1 Clash player hit dmg=19 php=76 ehp=274 roll=14/0 desc="" + [11] r3 seat=0 Clash enemy block dmg=3 php=131 ehp=274 roll=17/0 desc="" + [12] r3 seat=0 Clash enemy hit dmg=8 php=123 ehp=274 roll=12/0 desc="" + [13] r3 seat=0 Clash player hit dmg=10 php=123 ehp=264 roll=18/0 desc="" + [14] r4 seat=1 Clash player hit dmg=18 php=76 ehp=246 roll=19/0 desc="" + [15] r4 seat=1 Clash enemy hit dmg=9 php=67 ehp=246 roll=13/0 desc="" + [16] r4 seat=0 Clash enemy hit dmg=7 php=116 ehp=246 roll=8/0 desc="" + [17] r4 seat=0 Clash player hit dmg=11 php=116 ehp=235 roll=5/0 desc="" + [18] r5 seat=1 Decisive player hit dmg=17 php=67 ehp=218 roll=7/0 desc="" + [19] r5 seat=0 Decisive player hit dmg=11 php=116 ehp=207 roll=4/0 desc="" + [20] r5 seat=0 Decisive enemy hit dmg=8 php=108 ehp=207 roll=19/0 desc="" + [21] r5 seat=0 Decisive enemy crit dmg=16 php=92 ehp=207 roll=20/0 desc="" + [22] r5 seat=0 Decisive enemy hit dmg=8 php=84 ehp=207 roll=2/0 desc="" + [23] r6 seat=1 Sudden Death player hit dmg=20 php=67 ehp=187 roll=11/0 desc="" + [24] r6 seat=0 Sudden Death player hit dmg=13 php=84 ehp=174 roll=14/0 desc="" + [25] r6 seat=1 Sudden Death enemy hit dmg=10 php=57 ehp=174 roll=9/0 desc="" + [26] r6 seat=1 Sudden Death enemy hit dmg=9 php=48 ehp=174 roll=11/0 desc="" + [27] r6 seat=0 Sudden Death enemy hit dmg=10 php=74 ehp=174 roll=12/0 desc="" + [28] r7 seat=1 Sudden Death player hit dmg=22 php=48 ehp=152 roll=3/0 desc="" + [29] r7 seat=1 Sudden Death enemy hit dmg=10 php=38 ehp=152 roll=14/0 desc="" + [30] r7 seat=1 Sudden Death enemy hit dmg=8 php=30 ehp=152 roll=13/0 desc="" + [31] r7 seat=0 Sudden Death player hit dmg=14 php=74 ehp=138 roll=5/0 desc="" + [32] r8 seat=1 Sudden Death player hit dmg=22 php=30 ehp=116 roll=17/0 desc="" + [33] r8 seat=0 Sudden Death player hit dmg=14 php=74 ehp=102 roll=15/0 desc="" + [34] r8 seat=1 Sudden Death enemy hit dmg=11 php=19 ehp=102 roll=15/0 desc="" + [35] r8 seat=0 Sudden Death enemy hit dmg=9 php=65 ehp=102 roll=8/0 desc="" + [36] r9 seat=1 Sudden Death player hit dmg=19 php=19 ehp=83 roll=6/0 desc="" + [37] r9 seat=1 Sudden Death enemy hit dmg=11 php=8 ehp=83 roll=15/0 desc="" + [38] r9 seat=0 Sudden Death enemy crit dmg=18 php=47 ehp=83 roll=20/0 desc="" + [39] r9 seat=0 Sudden Death player hit dmg=15 php=47 ehp=68 roll=14/0 desc="" + [40] r9 seat=0 exhaust system timeout dmg=0 php=47 ehp=68 roll=0/0 desc="" + +=== duo/even seed=7 === +result: won=false timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=50 + seat[0]: start=160 entry=160 end=31 + seat[1]: start=90 entry=90 end=0 + [00] r1 seat=1 Opening player hit dmg=15 php=90 ehp=330 roll=10/0 desc="" + [01] r1 seat=0 Opening player hit dmg=10 php=160 ehp=320 roll=3/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=5 php=155 ehp=320 roll=16/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=6 php=84 ehp=320 roll=19/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=149 ehp=320 roll=11/0 desc="" + [05] r2 seat=1 Clash player hit dmg=16 php=84 ehp=304 roll=6/0 desc="" + [06] r2 seat=0 Clash player hit dmg=10 php=149 ehp=294 roll=18/0 desc="" + [07] r2 seat=1 Clash enemy hit dmg=8 php=76 ehp=294 roll=4/0 desc="" + [08] r2 seat=0 Clash enemy hit dmg=7 php=142 ehp=294 roll=11/0 desc="" + [09] r2 seat=0 Clash enemy hit dmg=8 php=134 ehp=294 roll=13/0 desc="" + [10] r2 seat=0 Clash environment environmental dmg=5 php=129 ehp=294 roll=0/0 desc="" + [11] r3 seat=1 Clash player hit dmg=15 php=76 ehp=279 roll=7/0 desc="" + [12] r3 seat=0 Clash enemy hit dmg=7 php=122 ehp=279 roll=2/0 desc="" + [13] r3 seat=0 Clash enemy hit dmg=6 php=116 ehp=279 roll=15/0 desc="" + [14] r3 seat=0 Clash player hit dmg=12 php=116 ehp=267 roll=11/0 desc="" + [15] r4 seat=1 Clash player hit dmg=18 php=76 ehp=249 roll=18/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=7 php=69 ehp=249 roll=4/0 desc="" + [17] r4 seat=1 Clash enemy hit dmg=7 php=62 ehp=249 roll=8/0 desc="" + [18] r4 seat=0 Clash player block dmg=5 php=116 ehp=244 roll=10/0 desc="" + [19] r5 seat=1 Decisive player hit dmg=20 php=62 ehp=224 roll=3/0 desc="" + [20] r5 seat=0 Decisive player hit dmg=12 php=116 ehp=212 roll=17/0 desc="" + [21] r5 seat=0 Decisive enemy block dmg=4 php=112 ehp=212 roll=4/0 desc="" + [22] r5 seat=0 Decisive enemy hit dmg=8 php=104 ehp=212 roll=5/0 desc="" + [23] r6 seat=1 Sudden Death player crit dmg=36 php=62 ehp=176 roll=20/0 desc="" + [24] r6 seat=0 Sudden Death player hit dmg=14 php=104 ehp=162 roll=13/0 desc="" + [25] r6 seat=1 Sudden Death enemy miss dmg=0 php=62 ehp=162 roll=1/0 desc="fumble" + [26] r6 seat=1 Sudden Death enemy hit dmg=9 php=53 ehp=162 roll=16/0 desc="" + [27] r6 seat=1 Sudden Death enemy hit dmg=11 php=42 ehp=162 roll=19/0 desc="" + [28] r7 seat=1 Sudden Death player miss dmg=0 php=42 ehp=162 roll=1/0 desc="fumble" + [29] r7 seat=0 Sudden Death player hit dmg=11 php=104 ehp=151 roll=8/0 desc="" + [30] r7 seat=0 Sudden Death enemy hit dmg=8 php=96 ehp=151 roll=15/0 desc="" + [31] r7 seat=1 Sudden Death enemy hit dmg=10 php=32 ehp=151 roll=5/0 desc="" + [32] r8 seat=1 Sudden Death player hit dmg=21 php=32 ehp=130 roll=8/0 desc="" + [33] r8 seat=0 Sudden Death player hit dmg=15 php=96 ehp=115 roll=3/0 desc="" + [34] r8 seat=0 Sudden Death enemy hit dmg=7 php=89 ehp=115 roll=14/0 desc="" + [35] r8 seat=0 Sudden Death enemy hit dmg=8 php=81 ehp=115 roll=17/0 desc="" + [36] r9 seat=1 Sudden Death player hit dmg=20 php=32 ehp=95 roll=15/0 desc="" + [37] r9 seat=1 Sudden Death enemy hit dmg=11 php=21 ehp=95 roll=14/0 desc="" + [38] r9 seat=1 Sudden Death enemy hit dmg=10 php=11 ehp=95 roll=13/0 desc="" + [39] r9 seat=0 Sudden Death enemy hit dmg=10 php=71 ehp=95 roll=9/0 desc="" + [40] r9 seat=0 Sudden Death player hit dmg=13 php=71 ehp=82 roll=19/0 desc="" + [41] r10 seat=1 Sudden Death player hit dmg=19 php=11 ehp=63 roll=3/0 desc="" + [42] r10 seat=0 Sudden Death player hit dmg=13 php=71 ehp=50 roll=2/0 desc="" + [43] r10 seat=0 Sudden Death enemy crit dmg=20 php=51 ehp=50 roll=20/0 desc="" + [44] r10 seat=1 Sudden Death enemy hit dmg=11 php=0 ehp=50 roll=18/0 desc="" + [45] r10 seat=0 Sudden Death enemy crit dmg=20 php=31 ehp=50 roll=20/0 desc="" + [46] r10 seat=0 exhaust system timeout dmg=0 php=31 ehp=50 roll=0/0 desc="" + +=== duo/even seed=42 === +result: won=false timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=100 + seat[0]: start=160 entry=160 end=67 + seat[1]: start=90 entry=90 end=0 + [00] r1 seat=1 Opening player hit dmg=12 php=90 ehp=333 roll=13/0 desc="" + [01] r1 seat=0 Opening player hit dmg=8 php=160 ehp=325 roll=5/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=325 roll=15/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=7 php=83 ehp=325 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=148 ehp=325 roll=13/0 desc="" + [05] r2 seat=1 Clash player hit dmg=19 php=83 ehp=306 roll=6/0 desc="" + [06] r2 seat=0 Clash player hit dmg=9 php=148 ehp=297 roll=12/0 desc="" + [07] r2 seat=0 Clash enemy hit dmg=7 php=141 ehp=297 roll=3/0 desc="" + [08] r2 seat=1 Clash enemy hit dmg=7 php=76 ehp=297 roll=5/0 desc="" + [09] r2 seat=0 Clash enemy block dmg=3 php=138 ehp=297 roll=15/0 desc="" + [10] r3 seat=1 Clash player hit dmg=15 php=76 ehp=282 roll=13/0 desc="" + [11] r3 seat=0 Clash player hit dmg=12 php=138 ehp=270 roll=12/0 desc="" + [12] r3 seat=1 Clash enemy hit dmg=9 php=67 ehp=270 roll=15/0 desc="" + [13] r3 seat=0 Clash enemy hit dmg=6 php=132 ehp=270 roll=4/0 desc="" + [14] r3 seat=0 Clash enemy hit dmg=7 php=125 ehp=270 roll=9/0 desc="" + [15] r4 seat=1 Clash player hit dmg=17 php=67 ehp=253 roll=10/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=8 php=59 ehp=253 roll=15/0 desc="" + [17] r4 seat=1 Clash enemy miss dmg=0 php=59 ehp=253 roll=1/0 desc="fumble" + [18] r4 seat=0 Clash player hit dmg=11 php=125 ehp=242 roll=9/0 desc="" + [19] r5 seat=1 Decisive player hit dmg=17 php=59 ehp=225 roll=3/0 desc="" + [20] r5 seat=0 Decisive player hit dmg=11 php=125 ehp=214 roll=15/0 desc="" + [21] r5 seat=1 Decisive enemy hit dmg=8 php=51 ehp=214 roll=15/0 desc="" + [22] r5 seat=1 Decisive enemy hit dmg=9 php=42 ehp=214 roll=19/0 desc="" + [23] r5 seat=1 Decisive enemy hit dmg=8 php=34 ehp=214 roll=3/0 desc="" + [24] r6 seat=1 Sudden Death player miss dmg=0 php=34 ehp=214 roll=1/0 desc="fumble" + [25] r6 seat=0 Sudden Death enemy miss dmg=0 php=125 ehp=214 roll=1/0 desc="fumble" + [26] r6 seat=0 Sudden Death enemy hit dmg=10 php=115 ehp=214 roll=11/0 desc="" + [27] r6 seat=1 Sudden Death enemy hit dmg=11 php=23 ehp=214 roll=14/0 desc="" + [28] r6 seat=0 Sudden Death player hit dmg=14 php=115 ehp=200 roll=7/0 desc="" + [29] r7 seat=1 Sudden Death player hit dmg=22 php=23 ehp=178 roll=9/0 desc="" + [30] r7 seat=0 Sudden Death player hit dmg=15 php=115 ehp=163 roll=15/0 desc="" + [31] r7 seat=0 Sudden Death enemy hit dmg=8 php=107 ehp=163 roll=6/0 desc="" + [32] r7 seat=0 Sudden Death enemy hit dmg=10 php=97 ehp=163 roll=3/0 desc="" + [33] r7 seat=1 Sudden Death enemy hit dmg=9 php=14 ehp=163 roll=8/0 desc="" + [34] r8 seat=0 Sudden Death player hit dmg=12 php=97 ehp=151 roll=7/0 desc="" + [35] r8 seat=1 Sudden Death player miss dmg=0 php=14 ehp=151 roll=1/0 desc="fumble" + [36] r8 seat=1 Sudden Death enemy hit dmg=11 php=3 ehp=151 roll=16/0 desc="" + [37] r8 seat=0 Sudden Death enemy hit dmg=8 php=89 ehp=151 roll=17/0 desc="" + [38] r9 seat=1 Sudden Death player hit dmg=23 php=3 ehp=128 roll=11/0 desc="" + [39] r9 seat=0 Sudden Death player hit dmg=15 php=89 ehp=113 roll=8/0 desc="" + [40] r9 seat=1 Sudden Death enemy block dmg=5 php=0 ehp=113 roll=11/0 desc="" + [41] r9 seat=0 Sudden Death enemy hit dmg=10 php=79 ehp=113 roll=7/0 desc="" + [42] r9 seat=0 Sudden Death enemy block dmg=4 php=75 ehp=113 roll=4/0 desc="" + [43] r10 seat=0 Sudden Death player hit dmg=13 php=75 ehp=100 roll=9/0 desc="" + [44] r10 seat=0 Sudden Death enemy hit dmg=8 php=67 ehp=100 roll=10/0 desc="" + [45] r10 seat=0 exhaust system timeout dmg=0 php=67 ehp=100 roll=0/0 desc="" + +=== duo/tank+support seed=1 === +result: won=false timedOut=true rounds=9 survivors=true + enemy: start=345 entry=345 end=135 + seat[0]: start=160 entry=160 end=87 + seat[1]: start=110 entry=110 end=0 + [00] r1 seat=0 Opening player hit dmg=10 php=160 ehp=335 roll=5/0 desc="" + [01] r1 seat=1 Opening player hit dmg=7 php=110 ehp=328 roll=5/0 desc="" + [02] r1 seat=1 Opening enemy hit dmg=6 php=104 ehp=328 roll=11/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=5 php=99 ehp=328 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=328 roll=19/0 desc="" + [05] r1 seat=0 Opening environment environmental dmg=5 php=149 ehp=328 roll=0/0 desc="" + [06] r2 seat=1 Clash enemy hit dmg=7 php=92 ehp=328 roll=9/0 desc="" + [07] r2 seat=1 Clash enemy hit dmg=7 php=85 ehp=328 roll=13/0 desc="" + [08] r2 seat=1 Clash enemy block dmg=3 php=82 ehp=328 roll=19/0 desc="" + [09] r2 seat=0 Clash player hit dmg=10 php=149 ehp=318 roll=4/0 desc="" + [10] r2 seat=1 Clash player hit dmg=9 php=82 ehp=309 roll=6/0 desc="" + [11] r2 seat=0 Clash environment environmental dmg=2 php=147 ehp=309 roll=0/0 desc="" + [12] r3 seat=1 Clash enemy miss dmg=0 php=82 ehp=309 roll=1/0 desc="fumble" + [13] r3 seat=1 Clash enemy hit dmg=8 php=74 ehp=309 roll=6/0 desc="" + [14] r3 seat=0 Clash player hit dmg=11 php=147 ehp=298 roll=5/0 desc="" + [15] r3 seat=1 Clash player crit dmg=18 php=74 ehp=280 roll=20/0 desc="" + [16] r4 seat=0 Clash player crit dmg=22 php=147 ehp=258 roll=20/0 desc="" + [17] r4 seat=1 Clash enemy hit dmg=8 php=66 ehp=258 roll=2/0 desc="" + [18] r4 seat=0 Clash enemy crit dmg=16 php=131 ehp=258 roll=20/0 desc="" + [19] r4 seat=1 Clash player hit dmg=7 php=66 ehp=251 roll=8/0 desc="" + [20] r4 seat=1 Clash environment environmental dmg=5 php=61 ehp=251 roll=0/0 desc="" + [21] r5 seat=1 Decisive player hit dmg=9 php=61 ehp=242 roll=18/0 desc="" + [22] r5 seat=0 Decisive player hit dmg=11 php=131 ehp=231 roll=19/0 desc="" + [23] r5 seat=1 Decisive enemy hit dmg=9 php=52 ehp=231 roll=12/0 desc="" + [24] r5 seat=0 Decisive enemy hit dmg=7 php=124 ehp=231 roll=8/0 desc="" + [25] r5 seat=0 Decisive enemy hit dmg=8 php=116 ehp=231 roll=2/0 desc="" + [26] r6 seat=1 Sudden Death enemy hit dmg=10 php=42 ehp=231 roll=12/0 desc="" + [27] r6 seat=1 Sudden Death enemy hit dmg=10 php=32 ehp=231 roll=14/0 desc="" + [28] r6 seat=1 Sudden Death enemy hit dmg=10 php=22 ehp=231 roll=13/0 desc="" + [29] r6 seat=1 Sudden Death player hit dmg=11 php=22 ehp=220 roll=16/0 desc="" + [30] r6 seat=0 Sudden Death player hit dmg=14 php=116 ehp=206 roll=10/0 desc="" + [31] r6 seat=0 Sudden Death environment environmental dmg=3 php=113 ehp=206 roll=0/0 desc="" + [32] r7 seat=1 Sudden Death player hit dmg=9 php=22 ehp=197 roll=14/0 desc="" + [33] r7 seat=0 Sudden Death player hit dmg=14 php=113 ehp=183 roll=13/0 desc="" + [34] r7 seat=0 Sudden Death enemy hit dmg=8 php=105 ehp=183 roll=9/0 desc="" + [35] r7 seat=1 Sudden Death enemy hit dmg=9 php=13 ehp=183 roll=7/0 desc="" + [36] r8 seat=1 Sudden Death player hit dmg=10 php=13 ehp=173 roll=16/0 desc="" + [37] r8 seat=0 Sudden Death player hit dmg=14 php=105 ehp=159 roll=19/0 desc="" + [38] r8 seat=0 Sudden Death enemy hit dmg=10 php=95 ehp=159 roll=11/0 desc="" + [39] r8 seat=1 Sudden Death enemy miss dmg=0 php=13 ehp=159 roll=1/0 desc="fumble" + [40] r8 seat=0 Sudden Death enemy hit dmg=8 php=87 ehp=159 roll=6/0 desc="" + [41] r9 seat=0 Sudden Death player hit dmg=14 php=87 ehp=145 roll=11/0 desc="" + [42] r9 seat=1 Sudden Death player hit dmg=10 php=13 ehp=135 roll=13/0 desc="" + [43] r9 seat=1 Sudden Death enemy block dmg=4 php=9 ehp=135 roll=5/0 desc="" + [44] r9 seat=1 Sudden Death enemy crit dmg=18 php=0 ehp=135 roll=20/0 desc="" + [45] r9 seat=0 exhaust system timeout dmg=0 php=87 ehp=135 roll=0/0 desc="" + +=== duo/tank+support seed=2 === +result: won=false timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=140 + seat[0]: start=160 entry=160 end=5 + seat[1]: start=110 entry=110 end=47 + [00] r1 seat=1 Opening player hit dmg=8 php=110 ehp=337 roll=12/0 desc="" + [01] r1 seat=0 Opening player hit dmg=9 php=160 ehp=328 roll=3/0 desc="" + [02] r1 seat=0 Opening enemy crit dmg=14 php=146 ehp=328 roll=20/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=6 php=104 ehp=328 roll=12/0 desc="" + [04] r2 seat=0 Clash player hit dmg=11 php=146 ehp=317 roll=5/0 desc="" + [05] r2 seat=0 Clash enemy crit dmg=14 php=132 ehp=317 roll=20/0 desc="" + [06] r2 seat=1 Clash enemy hit dmg=9 php=95 ehp=317 roll=6/0 desc="" + [07] r2 seat=1 Clash player hit dmg=9 php=95 ehp=308 roll=10/0 desc="" + [08] r3 seat=1 Clash player hit dmg=9 php=95 ehp=299 roll=16/0 desc="" + [09] r3 seat=0 Clash enemy miss dmg=0 php=132 ehp=299 roll=1/0 desc="fumble" + [10] r3 seat=0 Clash enemy hit dmg=8 php=124 ehp=299 roll=9/0 desc="" + [11] r3 seat=0 Clash player hit dmg=11 php=124 ehp=288 roll=16/0 desc="" + [12] r4 seat=1 Clash player miss dmg=0 php=95 ehp=288 roll=1/0 desc="fumble" + [13] r4 seat=0 Clash player hit dmg=10 php=124 ehp=278 roll=19/0 desc="" + [14] r4 seat=0 Clash enemy hit dmg=6 php=118 ehp=278 roll=11/0 desc="" + [15] r4 seat=1 Clash enemy hit dmg=8 php=87 ehp=278 roll=7/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=8 php=79 ehp=278 roll=4/0 desc="" + [17] r5 seat=1 Decisive player hit dmg=10 php=79 ehp=268 roll=9/0 desc="" + [18] r5 seat=1 Decisive enemy hit dmg=8 php=71 ehp=268 roll=19/0 desc="" + [19] r5 seat=0 Decisive enemy hit dmg=8 php=110 ehp=268 roll=18/0 desc="" + [20] r5 seat=0 Decisive player hit dmg=12 php=110 ehp=256 roll=8/0 desc="" + [21] r6 seat=1 Sudden Death player block dmg=4 php=71 ehp=252 roll=10/0 desc="" + [22] r6 seat=0 Sudden Death player hit dmg=14 php=110 ehp=238 roll=16/0 desc="" + [23] r6 seat=1 Sudden Death enemy block dmg=4 php=67 ehp=238 roll=4/0 desc="" + [24] r6 seat=1 Sudden Death enemy hit dmg=10 php=57 ehp=238 roll=15/0 desc="" + [25] r6 seat=1 Sudden Death enemy hit dmg=10 php=47 ehp=238 roll=8/0 desc="" + [26] r7 seat=0 Sudden Death player hit dmg=13 php=110 ehp=225 roll=8/0 desc="" + [27] r7 seat=0 Sudden Death enemy crit dmg=20 php=90 ehp=225 roll=20/0 desc="" + [28] r7 seat=0 Sudden Death enemy hit dmg=10 php=80 ehp=225 roll=15/0 desc="" + [29] r7 seat=0 Sudden Death enemy hit dmg=10 php=70 ehp=225 roll=16/0 desc="" + [30] r7 seat=1 Sudden Death player hit dmg=9 php=47 ehp=216 roll=10/0 desc="" + [31] r8 seat=0 Sudden Death player hit dmg=15 php=70 ehp=201 roll=5/0 desc="" + [32] r8 seat=1 Sudden Death player crit dmg=24 php=47 ehp=177 roll=20/0 desc="" + [33] r8 seat=0 Sudden Death enemy hit dmg=10 php=60 ehp=177 roll=10/0 desc="" + [34] r8 seat=0 Sudden Death enemy hit dmg=10 php=50 ehp=177 roll=5/0 desc="" + [35] r8 seat=0 Sudden Death enemy hit dmg=9 php=41 ehp=177 roll=7/0 desc="" + [36] r9 seat=1 Sudden Death player hit dmg=11 php=47 ehp=166 roll=17/0 desc="" + [37] r9 seat=0 Sudden Death enemy hit dmg=9 php=32 ehp=166 roll=16/0 desc="" + [38] r9 seat=0 Sudden Death enemy hit dmg=9 php=23 ehp=166 roll=3/0 desc="" + [39] r9 seat=0 Sudden Death player block dmg=5 php=23 ehp=161 roll=16/0 desc="" + [40] r10 seat=0 Sudden Death enemy hit dmg=8 php=15 ehp=161 roll=5/0 desc="" + [41] r10 seat=0 Sudden Death enemy hit dmg=10 php=5 ehp=161 roll=8/0 desc="" + [42] r10 seat=1 Sudden Death player hit dmg=8 php=47 ehp=153 roll=2/0 desc="" + [43] r10 seat=0 Sudden Death player hit dmg=13 php=5 ehp=140 roll=14/0 desc="" + [44] r10 seat=0 exhaust system timeout dmg=0 php=5 ehp=140 roll=0/0 desc="" + +=== duo/tank+support seed=3 === +result: won=false timedOut=true rounds=9 survivors=true + enemy: start=345 entry=345 end=141 + seat[0]: start=160 entry=160 end=72 + seat[1]: start=110 entry=110 end=28 + [00] r1 seat=1 Opening player hit dmg=7 php=110 ehp=338 roll=11/0 desc="" + [01] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=338 roll=18/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=6 php=148 ehp=338 roll=10/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=7 php=103 ehp=338 roll=13/0 desc="" + [04] r1 seat=0 Opening player hit dmg=8 php=148 ehp=330 roll=5/0 desc="" + [05] r2 seat=0 Clash player hit dmg=12 php=148 ehp=318 roll=2/0 desc="" + [06] r2 seat=1 Clash player hit dmg=9 php=103 ehp=309 roll=19/0 desc="" + [07] r2 seat=0 Clash enemy hit dmg=8 php=140 ehp=309 roll=2/0 desc="" + [08] r2 seat=0 Clash enemy hit dmg=6 php=134 ehp=309 roll=6/0 desc="" + [09] r2 seat=1 Clash environment environmental dmg=6 php=97 ehp=309 roll=0/0 desc="" + [10] r3 seat=1 Clash player hit dmg=9 php=97 ehp=300 roll=14/0 desc="" + [11] r3 seat=0 Clash enemy block dmg=3 php=131 ehp=300 roll=17/0 desc="" + [12] r3 seat=0 Clash enemy hit dmg=8 php=123 ehp=300 roll=12/0 desc="" + [13] r3 seat=0 Clash player hit dmg=10 php=123 ehp=290 roll=18/0 desc="" + [14] r4 seat=1 Clash enemy hit dmg=8 php=89 ehp=290 roll=6/0 desc="" + [15] r4 seat=1 Clash enemy hit dmg=8 php=81 ehp=290 roll=9/0 desc="" + [16] r4 seat=1 Clash player hit dmg=8 php=81 ehp=282 roll=8/0 desc="" + [17] r4 seat=0 Clash player hit dmg=11 php=123 ehp=271 roll=5/0 desc="" + [18] r5 seat=1 Decisive player hit dmg=8 php=81 ehp=263 roll=7/0 desc="" + [19] r5 seat=0 Decisive player hit dmg=11 php=123 ehp=252 roll=4/0 desc="" + [20] r5 seat=0 Decisive enemy hit dmg=8 php=115 ehp=252 roll=19/0 desc="" + [21] r5 seat=0 Decisive enemy crit dmg=16 php=99 ehp=252 roll=20/0 desc="" + [22] r5 seat=0 Decisive enemy hit dmg=8 php=91 ehp=252 roll=2/0 desc="" + [23] r6 seat=1 Sudden Death player hit dmg=10 php=81 ehp=242 roll=11/0 desc="" + [24] r6 seat=0 Sudden Death player hit dmg=13 php=91 ehp=229 roll=14/0 desc="" + [25] r6 seat=1 Sudden Death enemy hit dmg=9 php=72 ehp=229 roll=9/0 desc="" + [26] r6 seat=1 Sudden Death enemy hit dmg=9 php=63 ehp=229 roll=11/0 desc="" + [27] r6 seat=0 Sudden Death enemy hit dmg=10 php=81 ehp=229 roll=12/0 desc="" + [28] r7 seat=1 Sudden Death player hit dmg=11 php=63 ehp=218 roll=3/0 desc="" + [29] r7 seat=1 Sudden Death enemy hit dmg=9 php=54 ehp=218 roll=14/0 desc="" + [30] r7 seat=1 Sudden Death enemy hit dmg=8 php=46 ehp=218 roll=13/0 desc="" + [31] r7 seat=0 Sudden Death player hit dmg=14 php=81 ehp=204 roll=5/0 desc="" + [32] r8 seat=0 Sudden Death player hit dmg=14 php=81 ehp=190 roll=17/0 desc="" + [33] r8 seat=1 Sudden Death player hit dmg=11 php=46 ehp=179 roll=15/0 desc="" + [34] r8 seat=1 Sudden Death enemy hit dmg=10 php=36 ehp=179 roll=15/0 desc="" + [35] r8 seat=0 Sudden Death enemy hit dmg=9 php=72 ehp=179 roll=8/0 desc="" + [36] r9 seat=1 Sudden Death enemy miss dmg=0 php=36 ehp=179 roll=1/0 desc="fumble" + [37] r9 seat=1 Sudden Death enemy hit dmg=8 php=28 ehp=179 roll=12/0 desc="" + [38] r9 seat=1 Sudden Death player hit dmg=12 php=28 ehp=167 roll=6/0 desc="" + [39] r9 seat=0 Sudden Death player crit dmg=26 php=72 ehp=141 roll=20/0 desc="" + [40] r9 seat=0 exhaust system timeout dmg=0 php=72 ehp=141 roll=0/0 desc="" + +=== duo/tank+support seed=7 === +result: won=false timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=142 + seat[0]: start=160 entry=160 end=31 + seat[1]: start=110 entry=110 end=27 + [00] r1 seat=1 Opening player hit dmg=7 php=110 ehp=338 roll=10/0 desc="" + [01] r1 seat=0 Opening player hit dmg=10 php=160 ehp=328 roll=3/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=5 php=155 ehp=328 roll=16/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=5 php=105 ehp=328 roll=19/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=149 ehp=328 roll=11/0 desc="" + [05] r2 seat=1 Clash player hit dmg=8 php=105 ehp=320 roll=6/0 desc="" + [06] r2 seat=0 Clash player hit dmg=10 php=149 ehp=310 roll=18/0 desc="" + [07] r2 seat=1 Clash enemy hit dmg=7 php=98 ehp=310 roll=4/0 desc="" + [08] r2 seat=0 Clash enemy hit dmg=7 php=142 ehp=310 roll=11/0 desc="" + [09] r2 seat=0 Clash enemy hit dmg=8 php=134 ehp=310 roll=13/0 desc="" + [10] r2 seat=0 Clash environment environmental dmg=5 php=129 ehp=310 roll=0/0 desc="" + [11] r3 seat=1 Clash player hit dmg=7 php=98 ehp=303 roll=7/0 desc="" + [12] r3 seat=0 Clash enemy hit dmg=7 php=122 ehp=303 roll=2/0 desc="" + [13] r3 seat=0 Clash enemy hit dmg=6 php=116 ehp=303 roll=15/0 desc="" + [14] r3 seat=0 Clash player hit dmg=12 php=116 ehp=291 roll=11/0 desc="" + [15] r4 seat=1 Clash player hit dmg=9 php=98 ehp=282 roll=18/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=7 php=91 ehp=282 roll=4/0 desc="" + [17] r4 seat=1 Clash enemy hit dmg=7 php=84 ehp=282 roll=8/0 desc="" + [18] r4 seat=0 Clash player block dmg=5 php=116 ehp=277 roll=10/0 desc="" + [19] r5 seat=1 Decisive player hit dmg=10 php=84 ehp=267 roll=3/0 desc="" + [20] r5 seat=0 Decisive player hit dmg=12 php=116 ehp=255 roll=17/0 desc="" + [21] r5 seat=0 Decisive enemy block dmg=4 php=112 ehp=255 roll=4/0 desc="" + [22] r5 seat=0 Decisive enemy hit dmg=8 php=104 ehp=255 roll=5/0 desc="" + [23] r6 seat=1 Sudden Death player crit dmg=18 php=84 ehp=237 roll=20/0 desc="" + [24] r6 seat=0 Sudden Death player hit dmg=14 php=104 ehp=223 roll=13/0 desc="" + [25] r6 seat=1 Sudden Death enemy miss dmg=0 php=84 ehp=223 roll=1/0 desc="fumble" + [26] r6 seat=1 Sudden Death enemy hit dmg=8 php=76 ehp=223 roll=16/0 desc="" + [27] r6 seat=1 Sudden Death enemy hit dmg=10 php=66 ehp=223 roll=19/0 desc="" + [28] r7 seat=1 Sudden Death player miss dmg=0 php=66 ehp=223 roll=1/0 desc="fumble" + [29] r7 seat=0 Sudden Death player hit dmg=11 php=104 ehp=212 roll=8/0 desc="" + [30] r7 seat=0 Sudden Death enemy hit dmg=8 php=96 ehp=212 roll=15/0 desc="" + [31] r7 seat=1 Sudden Death enemy hit dmg=9 php=57 ehp=212 roll=5/0 desc="" + [32] r8 seat=1 Sudden Death player hit dmg=10 php=57 ehp=202 roll=8/0 desc="" + [33] r8 seat=0 Sudden Death player hit dmg=15 php=96 ehp=187 roll=3/0 desc="" + [34] r8 seat=0 Sudden Death enemy hit dmg=7 php=89 ehp=187 roll=14/0 desc="" + [35] r8 seat=0 Sudden Death enemy hit dmg=8 php=81 ehp=187 roll=17/0 desc="" + [36] r9 seat=1 Sudden Death player hit dmg=10 php=57 ehp=177 roll=15/0 desc="" + [37] r9 seat=1 Sudden Death enemy hit dmg=10 php=47 ehp=177 roll=14/0 desc="" + [38] r9 seat=1 Sudden Death enemy hit dmg=10 php=37 ehp=177 roll=13/0 desc="" + [39] r9 seat=0 Sudden Death enemy hit dmg=10 php=71 ehp=177 roll=9/0 desc="" + [40] r9 seat=0 Sudden Death player hit dmg=13 php=71 ehp=164 roll=19/0 desc="" + [41] r10 seat=0 Sudden Death player hit dmg=12 php=71 ehp=152 roll=3/0 desc="" + [42] r10 seat=1 Sudden Death player hit dmg=10 php=37 ehp=142 roll=2/0 desc="" + [43] r10 seat=0 Sudden Death enemy crit dmg=20 php=51 ehp=142 roll=20/0 desc="" + [44] r10 seat=1 Sudden Death enemy hit dmg=10 php=27 ehp=142 roll=18/0 desc="" + [45] r10 seat=0 Sudden Death enemy crit dmg=20 php=31 ehp=142 roll=20/0 desc="" + [46] r10 seat=0 exhaust system timeout dmg=0 php=31 ehp=142 roll=0/0 desc="" + +=== duo/tank+support seed=42 === +result: won=false timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=140 + seat[0]: start=160 entry=160 end=47 + seat[1]: start=110 entry=110 end=9 + [00] r1 seat=0 Opening player hit dmg=8 php=160 ehp=337 roll=13/0 desc="" + [01] r1 seat=1 Opening player hit dmg=6 php=110 ehp=331 roll=5/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=331 roll=15/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=7 php=103 ehp=331 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=148 ehp=331 roll=13/0 desc="" + [05] r2 seat=1 Clash player hit dmg=9 php=103 ehp=322 roll=6/0 desc="" + [06] r2 seat=0 Clash player hit dmg=9 php=148 ehp=313 roll=12/0 desc="" + [07] r2 seat=0 Clash enemy hit dmg=7 php=141 ehp=313 roll=3/0 desc="" + [08] r2 seat=1 Clash enemy hit dmg=7 php=96 ehp=313 roll=5/0 desc="" + [09] r2 seat=0 Clash enemy block dmg=3 php=138 ehp=313 roll=15/0 desc="" + [10] r3 seat=1 Clash player hit dmg=7 php=96 ehp=306 roll=13/0 desc="" + [11] r3 seat=0 Clash player hit dmg=12 php=138 ehp=294 roll=12/0 desc="" + [12] r3 seat=1 Clash enemy hit dmg=9 php=87 ehp=294 roll=15/0 desc="" + [13] r3 seat=0 Clash enemy hit dmg=6 php=132 ehp=294 roll=4/0 desc="" + [14] r3 seat=0 Clash enemy hit dmg=7 php=125 ehp=294 roll=9/0 desc="" + [15] r4 seat=1 Clash player hit dmg=8 php=87 ehp=286 roll=10/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=8 php=79 ehp=286 roll=15/0 desc="" + [17] r4 seat=1 Clash enemy miss dmg=0 php=79 ehp=286 roll=1/0 desc="fumble" + [18] r4 seat=0 Clash player hit dmg=11 php=125 ehp=275 roll=9/0 desc="" + [19] r5 seat=1 Decisive player hit dmg=8 php=79 ehp=267 roll=3/0 desc="" + [20] r5 seat=0 Decisive player hit dmg=11 php=125 ehp=256 roll=15/0 desc="" + [21] r5 seat=1 Decisive enemy hit dmg=7 php=72 ehp=256 roll=15/0 desc="" + [22] r5 seat=1 Decisive enemy hit dmg=9 php=63 ehp=256 roll=19/0 desc="" + [23] r5 seat=1 Decisive enemy hit dmg=8 php=55 ehp=256 roll=3/0 desc="" + [24] r6 seat=0 Sudden Death enemy hit dmg=10 php=115 ehp=256 roll=4/0 desc="" + [25] r6 seat=1 Sudden Death enemy hit dmg=9 php=46 ehp=256 roll=18/0 desc="" + [26] r6 seat=1 Sudden Death enemy hit dmg=9 php=37 ehp=256 roll=17/0 desc="" + [27] r6 seat=1 Sudden Death player crit dmg=22 php=37 ehp=234 roll=20/0 desc="" + [28] r6 seat=0 Sudden Death player hit dmg=12 php=115 ehp=222 roll=11/0 desc="" + [29] r7 seat=1 Sudden Death player hit dmg=10 php=37 ehp=212 roll=14/0 desc="" + [30] r7 seat=0 Sudden Death player hit dmg=11 php=115 ehp=201 roll=6/0 desc="" + [31] r7 seat=0 Sudden Death enemy hit dmg=10 php=105 ehp=201 roll=3/0 desc="" + [32] r7 seat=1 Sudden Death enemy hit dmg=8 php=29 ehp=201 roll=8/0 desc="" + [33] r7 seat=0 Sudden Death enemy hit dmg=8 php=97 ehp=201 roll=4/0 desc="" + [34] r7 seat=1 Sudden Death environment environmental dmg=3 php=26 ehp=201 roll=0/0 desc="" + [35] r8 seat=1 Sudden Death player hit dmg=11 php=26 ehp=190 roll=16/0 desc="" + [36] r8 seat=0 Sudden Death player hit dmg=12 php=97 ehp=178 roll=17/0 desc="" + [37] r8 seat=0 Sudden Death enemy hit dmg=9 php=88 ehp=178 roll=10/0 desc="" + [38] r8 seat=1 Sudden Death enemy hit dmg=9 php=17 ehp=178 roll=16/0 desc="" + [39] r9 seat=0 Sudden Death player block dmg=7 php=88 ehp=171 roll=11/0 desc="" + [40] r9 seat=1 Sudden Death player hit dmg=9 php=17 ehp=162 roll=9/0 desc="" + [41] r9 seat=1 Sudden Death enemy miss dmg=0 php=17 ehp=162 roll=1/0 desc="fumble" + [42] r9 seat=1 Sudden Death enemy hit dmg=8 php=9 ehp=162 roll=12/0 desc="" + [43] r9 seat=0 Sudden Death enemy hit dmg=9 php=79 ehp=162 roll=9/0 desc="" + [44] r10 seat=1 Sudden Death player hit dmg=10 php=9 ehp=152 roll=14/0 desc="" + [45] r10 seat=0 Sudden Death player hit dmg=12 php=79 ehp=140 roll=19/0 desc="" + [46] r10 seat=0 Sudden Death enemy miss dmg=0 php=79 ehp=140 roll=1/0 desc="fumble" + [47] r10 seat=0 Sudden Death enemy hit dmg=9 php=70 ehp=140 roll=14/0 desc="" + [48] r10 seat=0 Sudden Death enemy crit dmg=20 php=50 ehp=140 roll=20/0 desc="" + [49] r10 seat=0 Sudden Death environment environmental dmg=3 php=47 ehp=140 roll=0/0 desc="" + [50] r10 seat=0 exhaust system timeout dmg=0 php=47 ehp=140 roll=0/0 desc="" + +=== duo/median+weak seed=1 === +result: won=false timedOut=true rounds=9 survivors=true + enemy: start=345 entry=345 end=205 + seat[0]: start=160 entry=160 end=71 + seat[1]: start=70 entry=70 end=0 + [00] r1 seat=0 Opening player hit dmg=10 php=160 ehp=335 roll=5/0 desc="" + [01] r1 seat=1 Opening player hit dmg=5 php=70 ehp=330 roll=5/0 desc="" + [02] r1 seat=1 Opening enemy hit dmg=7 php=63 ehp=330 roll=11/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=6 php=57 ehp=330 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=330 roll=19/0 desc="" + [05] r1 seat=0 Opening environment environmental dmg=5 php=149 ehp=330 roll=0/0 desc="" + [06] r2 seat=1 Clash enemy hit dmg=8 php=49 ehp=330 roll=9/0 desc="" + [07] r2 seat=1 Clash enemy hit dmg=8 php=41 ehp=330 roll=13/0 desc="" + [08] r2 seat=1 Clash enemy block dmg=3 php=38 ehp=330 roll=19/0 desc="" + [09] r2 seat=0 Clash player hit dmg=10 php=149 ehp=320 roll=4/0 desc="" + [10] r2 seat=1 Clash player hit dmg=7 php=38 ehp=313 roll=6/0 desc="" + [11] r2 seat=0 Clash environment environmental dmg=2 php=147 ehp=313 roll=0/0 desc="" + [12] r3 seat=1 Clash enemy miss dmg=0 php=38 ehp=313 roll=1/0 desc="fumble" + [13] r3 seat=1 Clash enemy hit dmg=9 php=29 ehp=313 roll=6/0 desc="" + [14] r3 seat=0 Clash player hit dmg=11 php=147 ehp=302 roll=5/0 desc="" + [15] r3 seat=1 Clash player crit dmg=14 php=29 ehp=288 roll=20/0 desc="" + [16] r4 seat=0 Clash player crit dmg=22 php=147 ehp=266 roll=20/0 desc="" + [17] r4 seat=1 Clash enemy hit dmg=9 php=20 ehp=266 roll=2/0 desc="" + [18] r4 seat=0 Clash enemy crit dmg=16 php=131 ehp=266 roll=20/0 desc="" + [19] r4 seat=1 Clash player hit dmg=5 php=20 ehp=261 roll=8/0 desc="" + [20] r4 seat=1 Clash environment environmental dmg=5 php=15 ehp=261 roll=0/0 desc="" + [21] r5 seat=1 Decisive player hit dmg=6 php=15 ehp=255 roll=18/0 desc="" + [22] r5 seat=0 Decisive player hit dmg=11 php=131 ehp=244 roll=19/0 desc="" + [23] r5 seat=1 Decisive enemy hit dmg=10 php=5 ehp=244 roll=12/0 desc="" + [24] r5 seat=0 Decisive enemy hit dmg=7 php=124 ehp=244 roll=8/0 desc="" + [25] r5 seat=0 Decisive enemy hit dmg=8 php=116 ehp=244 roll=2/0 desc="" + [26] r6 seat=1 Sudden Death enemy hit dmg=10 php=0 ehp=244 roll=12/0 desc="" + [27] r6 seat=0 Sudden Death enemy hit dmg=9 php=107 ehp=244 roll=14/0 desc="" + [28] r6 seat=0 Sudden Death enemy hit dmg=9 php=98 ehp=244 roll=13/0 desc="" + [29] r6 seat=0 Sudden Death player hit dmg=15 php=98 ehp=229 roll=16/0 desc="" + [30] r7 seat=0 Sudden Death enemy hit dmg=10 php=88 ehp=229 roll=5/0 desc="" + [31] r7 seat=0 Sudden Death player miss dmg=0 php=88 ehp=229 roll=1/0 desc="fumble" + [32] r8 seat=0 Sudden Death enemy hit dmg=8 php=80 ehp=229 roll=6/0 desc="" + [33] r8 seat=0 Sudden Death player hit dmg=12 php=80 ehp=217 roll=9/0 desc="" + [34] r9 seat=0 Sudden Death player hit dmg=12 php=80 ehp=205 roll=10/0 desc="" + [35] r9 seat=0 Sudden Death enemy hit dmg=9 php=71 ehp=205 roll=17/0 desc="" + [36] r9 seat=0 exhaust system timeout dmg=0 php=71 ehp=205 roll=0/0 desc="" + +=== duo/median+weak seed=2 === +result: won=false timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=167 + seat[0]: start=160 entry=160 end=5 + seat[1]: start=70 entry=70 end=1 + [00] r1 seat=1 Opening player hit dmg=6 php=70 ehp=339 roll=12/0 desc="" + [01] r1 seat=0 Opening player hit dmg=9 php=160 ehp=330 roll=3/0 desc="" + [02] r1 seat=0 Opening enemy crit dmg=14 php=146 ehp=330 roll=20/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=7 php=63 ehp=330 roll=12/0 desc="" + [04] r2 seat=0 Clash player hit dmg=11 php=146 ehp=319 roll=5/0 desc="" + [05] r2 seat=0 Clash enemy crit dmg=14 php=132 ehp=319 roll=20/0 desc="" + [06] r2 seat=1 Clash enemy hit dmg=9 php=54 ehp=319 roll=6/0 desc="" + [07] r2 seat=1 Clash player hit dmg=6 php=54 ehp=313 roll=10/0 desc="" + [08] r3 seat=1 Clash player hit dmg=6 php=54 ehp=307 roll=16/0 desc="" + [09] r3 seat=0 Clash enemy miss dmg=0 php=132 ehp=307 roll=1/0 desc="fumble" + [10] r3 seat=0 Clash enemy hit dmg=8 php=124 ehp=307 roll=9/0 desc="" + [11] r3 seat=0 Clash player hit dmg=11 php=124 ehp=296 roll=16/0 desc="" + [12] r4 seat=1 Clash player miss dmg=0 php=54 ehp=296 roll=1/0 desc="fumble" + [13] r4 seat=0 Clash player hit dmg=10 php=124 ehp=286 roll=19/0 desc="" + [14] r4 seat=0 Clash enemy hit dmg=6 php=118 ehp=286 roll=11/0 desc="" + [15] r4 seat=1 Clash enemy hit dmg=9 php=45 ehp=286 roll=7/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=9 php=36 ehp=286 roll=4/0 desc="" + [17] r5 seat=1 Decisive player hit dmg=7 php=36 ehp=279 roll=9/0 desc="" + [18] r5 seat=1 Decisive enemy hit dmg=9 php=27 ehp=279 roll=19/0 desc="" + [19] r5 seat=0 Decisive enemy hit dmg=8 php=110 ehp=279 roll=18/0 desc="" + [20] r5 seat=0 Decisive player hit dmg=12 php=110 ehp=267 roll=8/0 desc="" + [21] r6 seat=1 Sudden Death player block dmg=3 php=27 ehp=264 roll=10/0 desc="" + [22] r6 seat=0 Sudden Death player hit dmg=14 php=110 ehp=250 roll=16/0 desc="" + [23] r6 seat=1 Sudden Death enemy block dmg=4 php=23 ehp=250 roll=4/0 desc="" + [24] r6 seat=1 Sudden Death enemy hit dmg=11 php=12 ehp=250 roll=15/0 desc="" + [25] r6 seat=1 Sudden Death enemy hit dmg=11 php=1 ehp=250 roll=8/0 desc="" + [26] r7 seat=0 Sudden Death player hit dmg=13 php=110 ehp=237 roll=8/0 desc="" + [27] r7 seat=0 Sudden Death enemy crit dmg=20 php=90 ehp=237 roll=20/0 desc="" + [28] r7 seat=0 Sudden Death enemy hit dmg=10 php=80 ehp=237 roll=15/0 desc="" + [29] r7 seat=0 Sudden Death enemy hit dmg=10 php=70 ehp=237 roll=16/0 desc="" + [30] r7 seat=1 Sudden Death player hit dmg=7 php=1 ehp=230 roll=10/0 desc="" + [31] r8 seat=0 Sudden Death player hit dmg=15 php=70 ehp=215 roll=5/0 desc="" + [32] r8 seat=1 Sudden Death player crit dmg=16 php=1 ehp=199 roll=20/0 desc="" + [33] r8 seat=0 Sudden Death enemy hit dmg=10 php=60 ehp=199 roll=10/0 desc="" + [34] r8 seat=0 Sudden Death enemy hit dmg=10 php=50 ehp=199 roll=5/0 desc="" + [35] r8 seat=0 Sudden Death enemy hit dmg=9 php=41 ehp=199 roll=7/0 desc="" + [36] r9 seat=1 Sudden Death player hit dmg=8 php=1 ehp=191 roll=17/0 desc="" + [37] r9 seat=0 Sudden Death enemy hit dmg=9 php=32 ehp=191 roll=16/0 desc="" + [38] r9 seat=0 Sudden Death enemy hit dmg=9 php=23 ehp=191 roll=3/0 desc="" + [39] r9 seat=0 Sudden Death player block dmg=5 php=23 ehp=186 roll=16/0 desc="" + [40] r10 seat=0 Sudden Death enemy hit dmg=8 php=15 ehp=186 roll=5/0 desc="" + [41] r10 seat=0 Sudden Death enemy hit dmg=10 php=5 ehp=186 roll=8/0 desc="" + [42] r10 seat=1 Sudden Death player hit dmg=6 php=1 ehp=180 roll=2/0 desc="" + [43] r10 seat=0 Sudden Death player hit dmg=13 php=5 ehp=167 roll=14/0 desc="" + [44] r10 seat=0 exhaust system timeout dmg=0 php=5 ehp=167 roll=0/0 desc="" + +=== duo/median+weak seed=3 === +result: won=false timedOut=true rounds=9 survivors=true + enemy: start=345 entry=345 end=184 + seat[0]: start=160 entry=160 end=54 + seat[1]: start=70 entry=70 end=0 + [00] r1 seat=1 Opening player hit dmg=5 php=70 ehp=340 roll=11/0 desc="" + [01] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=340 roll=18/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=6 php=148 ehp=340 roll=10/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=8 php=62 ehp=340 roll=13/0 desc="" + [04] r1 seat=0 Opening player hit dmg=8 php=148 ehp=332 roll=5/0 desc="" + [05] r2 seat=0 Clash player hit dmg=12 php=148 ehp=320 roll=2/0 desc="" + [06] r2 seat=1 Clash player hit dmg=6 php=62 ehp=314 roll=19/0 desc="" + [07] r2 seat=0 Clash enemy hit dmg=8 php=140 ehp=314 roll=2/0 desc="" + [08] r2 seat=0 Clash enemy hit dmg=6 php=134 ehp=314 roll=6/0 desc="" + [09] r2 seat=1 Clash environment environmental dmg=6 php=56 ehp=314 roll=0/0 desc="" + [10] r3 seat=1 Clash player hit dmg=7 php=56 ehp=307 roll=14/0 desc="" + [11] r3 seat=0 Clash enemy block dmg=3 php=131 ehp=307 roll=17/0 desc="" + [12] r3 seat=0 Clash enemy hit dmg=8 php=123 ehp=307 roll=12/0 desc="" + [13] r3 seat=0 Clash player hit dmg=10 php=123 ehp=297 roll=18/0 desc="" + [14] r4 seat=1 Clash enemy hit dmg=9 php=47 ehp=297 roll=6/0 desc="" + [15] r4 seat=1 Clash enemy hit dmg=8 php=39 ehp=297 roll=9/0 desc="" + [16] r4 seat=1 Clash player hit dmg=6 php=39 ehp=291 roll=8/0 desc="" + [17] r4 seat=0 Clash player hit dmg=11 php=123 ehp=280 roll=5/0 desc="" + [18] r5 seat=1 Decisive player hit dmg=6 php=39 ehp=274 roll=7/0 desc="" + [19] r5 seat=0 Decisive player hit dmg=11 php=123 ehp=263 roll=4/0 desc="" + [20] r5 seat=0 Decisive enemy hit dmg=8 php=115 ehp=263 roll=19/0 desc="" + [21] r5 seat=0 Decisive enemy crit dmg=16 php=99 ehp=263 roll=20/0 desc="" + [22] r5 seat=0 Decisive enemy hit dmg=8 php=91 ehp=263 roll=2/0 desc="" + [23] r6 seat=1 Sudden Death player hit dmg=7 php=39 ehp=256 roll=11/0 desc="" + [24] r6 seat=0 Sudden Death player hit dmg=13 php=91 ehp=243 roll=14/0 desc="" + [25] r6 seat=1 Sudden Death enemy hit dmg=10 php=29 ehp=243 roll=9/0 desc="" + [26] r6 seat=1 Sudden Death enemy hit dmg=9 php=20 ehp=243 roll=11/0 desc="" + [27] r6 seat=0 Sudden Death enemy hit dmg=10 php=81 ehp=243 roll=12/0 desc="" + [28] r7 seat=1 Sudden Death player hit dmg=8 php=20 ehp=235 roll=3/0 desc="" + [29] r7 seat=1 Sudden Death enemy hit dmg=10 php=10 ehp=235 roll=14/0 desc="" + [30] r7 seat=1 Sudden Death enemy hit dmg=9 php=1 ehp=235 roll=13/0 desc="" + [31] r7 seat=0 Sudden Death player hit dmg=14 php=81 ehp=221 roll=5/0 desc="" + [32] r8 seat=0 Sudden Death player hit dmg=14 php=81 ehp=207 roll=17/0 desc="" + [33] r8 seat=1 Sudden Death player hit dmg=8 php=1 ehp=199 roll=15/0 desc="" + [34] r8 seat=1 Sudden Death enemy hit dmg=11 php=0 ehp=199 roll=15/0 desc="" + [35] r8 seat=0 Sudden Death enemy hit dmg=9 php=72 ehp=199 roll=8/0 desc="" + [36] r9 seat=0 Sudden Death player hit dmg=15 php=72 ehp=184 roll=17/0 desc="" + [37] r9 seat=0 Sudden Death enemy crit dmg=18 php=54 ehp=184 roll=20/0 desc="" + [38] r9 seat=0 exhaust system timeout dmg=0 php=54 ehp=184 roll=0/0 desc="" + +=== duo/median+weak seed=7 === +result: won=false timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=175 + seat[0]: start=160 entry=160 end=55 + seat[1]: start=70 entry=70 end=0 + [00] r1 seat=1 Opening player hit dmg=5 php=70 ehp=340 roll=10/0 desc="" + [01] r1 seat=0 Opening player hit dmg=10 php=160 ehp=330 roll=3/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=5 php=155 ehp=330 roll=16/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=6 php=64 ehp=330 roll=19/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=149 ehp=330 roll=11/0 desc="" + [05] r2 seat=1 Clash player hit dmg=5 php=64 ehp=325 roll=6/0 desc="" + [06] r2 seat=0 Clash player hit dmg=10 php=149 ehp=315 roll=18/0 desc="" + [07] r2 seat=1 Clash enemy hit dmg=8 php=56 ehp=315 roll=4/0 desc="" + [08] r2 seat=0 Clash enemy hit dmg=7 php=142 ehp=315 roll=11/0 desc="" + [09] r2 seat=0 Clash enemy hit dmg=8 php=134 ehp=315 roll=13/0 desc="" + [10] r2 seat=0 Clash environment environmental dmg=5 php=129 ehp=315 roll=0/0 desc="" + [11] r3 seat=1 Clash player hit dmg=5 php=56 ehp=310 roll=7/0 desc="" + [12] r3 seat=0 Clash enemy hit dmg=7 php=122 ehp=310 roll=2/0 desc="" + [13] r3 seat=0 Clash enemy hit dmg=6 php=116 ehp=310 roll=15/0 desc="" + [14] r3 seat=0 Clash player hit dmg=12 php=116 ehp=298 roll=11/0 desc="" + [15] r4 seat=1 Clash enemy hit dmg=7 php=49 ehp=298 roll=15/0 desc="" + [16] r4 seat=0 Clash enemy hit dmg=8 php=108 ehp=298 roll=13/0 desc="" + [17] r4 seat=0 Clash player hit dmg=10 php=108 ehp=288 roll=8/0 desc="" + [18] r4 seat=1 Clash player block dmg=3 php=49 ehp=285 roll=10/0 desc="" + [19] r5 seat=1 Decisive player hit dmg=7 php=49 ehp=278 roll=3/0 desc="" + [20] r5 seat=0 Decisive player hit dmg=12 php=108 ehp=266 roll=17/0 desc="" + [21] r5 seat=0 Decisive enemy block dmg=4 php=104 ehp=266 roll=4/0 desc="" + [22] r5 seat=0 Decisive enemy hit dmg=8 php=96 ehp=266 roll=5/0 desc="" + [23] r6 seat=1 Sudden Death player crit dmg=12 php=49 ehp=254 roll=20/0 desc="" + [24] r6 seat=0 Sudden Death player hit dmg=14 php=96 ehp=240 roll=13/0 desc="" + [25] r6 seat=1 Sudden Death enemy miss dmg=0 php=49 ehp=240 roll=1/0 desc="fumble" + [26] r6 seat=1 Sudden Death enemy hit dmg=9 php=40 ehp=240 roll=16/0 desc="" + [27] r6 seat=1 Sudden Death enemy hit dmg=11 php=29 ehp=240 roll=19/0 desc="" + [28] r7 seat=1 Sudden Death player miss dmg=0 php=29 ehp=240 roll=1/0 desc="fumble" + [29] r7 seat=0 Sudden Death player hit dmg=11 php=96 ehp=229 roll=8/0 desc="" + [30] r7 seat=0 Sudden Death enemy hit dmg=8 php=88 ehp=229 roll=15/0 desc="" + [31] r7 seat=1 Sudden Death enemy hit dmg=10 php=19 ehp=229 roll=5/0 desc="" + [32] r8 seat=1 Sudden Death player hit dmg=7 php=19 ehp=222 roll=8/0 desc="" + [33] r8 seat=0 Sudden Death player hit dmg=15 php=88 ehp=207 roll=3/0 desc="" + [34] r8 seat=0 Sudden Death enemy hit dmg=7 php=81 ehp=207 roll=14/0 desc="" + [35] r8 seat=0 Sudden Death enemy hit dmg=8 php=73 ehp=207 roll=17/0 desc="" + [36] r9 seat=1 Sudden Death player hit dmg=7 php=19 ehp=200 roll=15/0 desc="" + [37] r9 seat=1 Sudden Death enemy hit dmg=11 php=8 ehp=200 roll=14/0 desc="" + [38] r9 seat=1 Sudden Death enemy hit dmg=11 php=0 ehp=200 roll=13/0 desc="" + [39] r9 seat=0 Sudden Death enemy hit dmg=10 php=63 ehp=200 roll=9/0 desc="" + [40] r9 seat=0 Sudden Death player hit dmg=13 php=63 ehp=187 roll=19/0 desc="" + [41] r10 seat=0 Sudden Death player hit dmg=12 php=63 ehp=175 roll=15/0 desc="" + [42] r10 seat=0 Sudden Death enemy hit dmg=8 php=55 ehp=175 roll=3/0 desc="" + [43] r10 seat=0 exhaust system timeout dmg=0 php=55 ehp=175 roll=0/0 desc="" + +=== duo/median+weak seed=42 === +result: won=false timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=186 + seat[0]: start=160 entry=160 end=79 + seat[1]: start=70 entry=70 end=0 + [00] r1 seat=0 Opening player hit dmg=8 php=160 ehp=337 roll=13/0 desc="" + [01] r1 seat=1 Opening player hit dmg=4 php=70 ehp=333 roll=5/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=333 roll=15/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=7 php=63 ehp=333 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=148 ehp=333 roll=13/0 desc="" + [05] r2 seat=1 Clash player hit dmg=7 php=63 ehp=326 roll=6/0 desc="" + [06] r2 seat=0 Clash player hit dmg=9 php=148 ehp=317 roll=12/0 desc="" + [07] r2 seat=0 Clash enemy hit dmg=7 php=141 ehp=317 roll=3/0 desc="" + [08] r2 seat=1 Clash enemy hit dmg=7 php=56 ehp=317 roll=5/0 desc="" + [09] r2 seat=0 Clash enemy block dmg=3 php=138 ehp=317 roll=15/0 desc="" + [10] r3 seat=1 Clash player hit dmg=5 php=56 ehp=312 roll=13/0 desc="" + [11] r3 seat=0 Clash player hit dmg=12 php=138 ehp=300 roll=12/0 desc="" + [12] r3 seat=1 Clash enemy hit dmg=10 php=46 ehp=300 roll=15/0 desc="" + [13] r3 seat=0 Clash enemy hit dmg=6 php=132 ehp=300 roll=4/0 desc="" + [14] r3 seat=0 Clash enemy hit dmg=7 php=125 ehp=300 roll=9/0 desc="" + [15] r4 seat=1 Clash player hit dmg=6 php=46 ehp=294 roll=10/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=8 php=38 ehp=294 roll=15/0 desc="" + [17] r4 seat=1 Clash enemy miss dmg=0 php=38 ehp=294 roll=1/0 desc="fumble" + [18] r4 seat=0 Clash player hit dmg=11 php=125 ehp=283 roll=9/0 desc="" + [19] r5 seat=1 Decisive player hit dmg=6 php=38 ehp=277 roll=3/0 desc="" + [20] r5 seat=0 Decisive player hit dmg=11 php=125 ehp=266 roll=15/0 desc="" + [21] r5 seat=1 Decisive enemy hit dmg=8 php=30 ehp=266 roll=15/0 desc="" + [22] r5 seat=1 Decisive enemy hit dmg=9 php=21 ehp=266 roll=19/0 desc="" + [23] r5 seat=1 Decisive enemy hit dmg=8 php=13 ehp=266 roll=3/0 desc="" + [24] r6 seat=0 Sudden Death enemy hit dmg=10 php=115 ehp=266 roll=4/0 desc="" + [25] r6 seat=1 Sudden Death enemy hit dmg=9 php=4 ehp=266 roll=18/0 desc="" + [26] r6 seat=1 Sudden Death enemy hit dmg=10 php=0 ehp=266 roll=17/0 desc="" + [27] r6 seat=0 Sudden Death player crit dmg=28 php=115 ehp=238 roll=20/0 desc="" + [28] r7 seat=0 Sudden Death player hit dmg=14 php=115 ehp=224 roll=9/0 desc="" + [29] r7 seat=0 Sudden Death enemy hit dmg=10 php=105 ehp=224 roll=15/0 desc="" + [30] r8 seat=0 Sudden Death player hit dmg=14 php=105 ehp=210 roll=3/0 desc="" + [31] r8 seat=0 Sudden Death enemy hit dmg=9 php=96 ehp=210 roll=18/0 desc="" + [32] r9 seat=0 Sudden Death enemy hit dmg=8 php=88 ehp=210 roll=18/0 desc="" + [33] r9 seat=0 Sudden Death player hit dmg=12 php=88 ehp=198 roll=3/0 desc="" + [34] r10 seat=0 Sudden Death player hit dmg=12 php=88 ehp=186 roll=17/0 desc="" + [35] r10 seat=0 Sudden Death enemy hit dmg=9 php=79 ehp=186 roll=10/0 desc="" + [36] r10 seat=0 exhaust system timeout dmg=0 php=79 ehp=186 roll=0/0 desc="" + +=== duo/glass falls early seed=1 === +result: won=false timedOut=false rounds=7 survivors=false + enemy: start=172 entry=172 end=64 + seat[0]: start=160 entry=160 end=0 + seat[1]: start=12 entry=12 end=0 + [00] r1 seat=1 Opening player hit dmg=15 php=12 ehp=157 roll=5/0 desc="" + [01] r1 seat=0 Opening player hit dmg=10 php=160 ehp=147 roll=5/0 desc="" + [02] r1 seat=1 Opening enemy hit dmg=22 php=0 ehp=147 roll=11/0 desc="" + [03] r1 seat=0 Opening enemy hit dmg=15 php=145 ehp=147 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=17 php=128 ehp=147 roll=19/0 desc="" + [05] r1 seat=0 Opening environment environmental dmg=5 php=123 ehp=147 roll=0/0 desc="" + [06] r2 seat=0 Clash player hit dmg=11 php=123 ehp=136 roll=18/0 desc="" + [07] r2 seat=0 Clash enemy crit dmg=46 php=77 ehp=136 roll=20/0 desc="" + [08] r3 seat=0 Clash player hit dmg=10 php=77 ehp=126 roll=8/0 desc="" + [09] r3 seat=0 Clash enemy hit dmg=20 php=57 ehp=126 roll=19/0 desc="" + [10] r4 seat=0 Decisive enemy hit dmg=22 php=35 ehp=126 roll=13/0 desc="" + [11] r4 seat=0 Decisive player hit dmg=14 php=35 ehp=112 roll=7/0 desc="" + [12] r5 seat=0 Sudden Death enemy miss dmg=0 php=35 ehp=112 roll=1/0 desc="fumble" + [13] r5 seat=0 Sudden Death player crit dmg=32 php=35 ehp=80 roll=20/0 desc="" + [14] r6 seat=0 Sudden Death player hit dmg=16 php=35 ehp=64 roll=3/0 desc="" + [15] r6 seat=0 Sudden Death enemy hit dmg=27 php=8 ehp=64 roll=19/0 desc="" + [16] r7 seat=0 Sudden Death enemy block dmg=12 php=0 ehp=64 roll=18/0 desc="" + +=== duo/glass falls early seed=2 === +result: won=false timedOut=false rounds=7 survivors=false + enemy: start=172 entry=172 end=95 + seat[0]: start=160 entry=160 end=0 + seat[1]: start=12 entry=12 end=0 + [00] r1 seat=1 Opening player hit dmg=15 php=12 ehp=157 roll=12/0 desc="" + [01] r1 seat=0 Opening player hit dmg=10 php=160 ehp=147 roll=3/0 desc="" + [02] r1 seat=0 Opening enemy crit dmg=40 php=120 ehp=147 roll=20/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=22 php=0 ehp=147 roll=12/0 desc="" + [04] r2 seat=0 Clash player hit dmg=12 php=120 ehp=135 roll=4/0 desc="" + [05] r2 seat=0 Clash enemy hit dmg=23 php=97 ehp=135 roll=2/0 desc="" + [06] r3 seat=0 Clash player hit dmg=11 php=97 ehp=124 roll=2/0 desc="" + [07] r3 seat=0 Clash enemy hit dmg=23 php=74 ehp=124 roll=17/0 desc="" + [08] r4 seat=0 Decisive enemy hit dmg=21 php=53 ehp=124 roll=7/0 desc="" + [09] r4 seat=0 Decisive player miss dmg=0 php=53 ehp=124 roll=1/0 desc="fumble" + [10] r5 seat=0 Sudden Death player hit dmg=13 php=53 ehp=111 roll=5/0 desc="" + [11] r5 seat=0 Sudden Death enemy hit dmg=25 php=28 ehp=111 roll=15/0 desc="" + [12] r6 seat=0 Sudden Death player hit dmg=16 php=28 ehp=95 roll=2/0 desc="" + [13] r6 seat=0 Sudden Death enemy hit dmg=25 php=3 ehp=95 roll=16/0 desc="" + [14] r7 seat=0 Sudden Death enemy hit dmg=23 php=0 ehp=95 roll=9/0 desc="" + +=== duo/glass falls early seed=3 === +result: won=false timedOut=false rounds=7 survivors=false + enemy: start=172 entry=172 end=101 + seat[0]: start=160 entry=160 end=0 + seat[1]: start=12 entry=12 end=0 + [00] r1 seat=1 Opening player hit dmg=12 php=12 ehp=160 roll=11/0 desc="" + [01] r1 seat=0 Opening enemy hit dmg=17 php=143 ehp=160 roll=18/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=19 php=124 ehp=160 roll=10/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=25 php=0 ehp=160 roll=13/0 desc="" + [04] r1 seat=0 Opening player hit dmg=9 php=124 ehp=151 roll=5/0 desc="" + [05] r2 seat=0 Clash player hit dmg=12 php=124 ehp=139 roll=16/0 desc="" + [06] r2 seat=0 Clash enemy hit dmg=20 php=104 ehp=139 roll=2/0 desc="" + [07] r2 seat=0 Clash environment environmental dmg=6 php=98 ehp=139 roll=0/0 desc="" + [08] r3 seat=0 Clash player hit dmg=11 php=98 ehp=128 roll=6/0 desc="" + [09] r3 seat=0 Clash enemy hit dmg=18 php=80 ehp=128 roll=4/0 desc="" + [10] r4 seat=0 Clash player hit dmg=13 php=80 ehp=115 roll=12/0 desc="" + [11] r4 seat=0 Clash enemy block dmg=9 php=71 ehp=115 roll=17/0 desc="" + [12] r5 seat=0 Decisive player miss dmg=0 php=71 ehp=115 roll=1/0 desc="fumble" + [13] r5 seat=0 Decisive enemy hit dmg=26 php=45 ehp=115 roll=7/0 desc="" + [14] r6 seat=0 Sudden Death player hit dmg=14 php=45 ehp=101 roll=16/0 desc="" + [15] r6 seat=0 Sudden Death enemy hit dmg=26 php=19 ehp=101 roll=9/0 desc="" + [16] r7 seat=0 Sudden Death enemy hit dmg=29 php=0 ehp=101 roll=11/0 desc="" + +=== duo/glass falls early seed=7 === +result: won=false timedOut=false rounds=7 survivors=false + enemy: start=172 entry=172 end=80 + seat[0]: start=160 entry=160 end=0 + seat[1]: start=12 entry=12 end=0 + [00] r1 seat=1 Opening player hit dmg=14 php=12 ehp=158 roll=10/0 desc="" + [01] r1 seat=0 Opening player hit dmg=11 php=160 ehp=147 roll=3/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=15 php=145 ehp=147 roll=16/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=19 php=0 ehp=147 roll=19/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=16 php=129 ehp=147 roll=11/0 desc="" + [05] r2 seat=0 Clash enemy hit dmg=24 php=105 ehp=147 roll=13/0 desc="" + [06] r2 seat=0 Clash player hit dmg=13 php=105 ehp=134 roll=8/0 desc="" + [07] r3 seat=0 Clash player hit dmg=14 php=105 ehp=120 roll=11/0 desc="" + [08] r3 seat=0 Clash enemy crit dmg=46 php=59 ehp=120 roll=20/0 desc="" + [09] r4 seat=0 Clash player hit dmg=13 php=59 ehp=107 roll=16/0 desc="" + [10] r4 seat=0 Clash enemy hit dmg=20 php=39 ehp=107 roll=10/0 desc="" + [11] r5 seat=0 Decisive player hit dmg=12 php=39 ehp=95 roll=16/0 desc="" + [12] r5 seat=0 Decisive enemy miss dmg=0 php=39 ehp=95 roll=1/0 desc="fumble" + [13] r6 seat=0 Sudden Death enemy hit dmg=24 php=15 ehp=95 roll=9/0 desc="" + [14] r6 seat=0 Sudden Death player hit dmg=15 php=15 ehp=80 roll=18/0 desc="" + [15] r7 seat=0 Sudden Death enemy hit dmg=23 php=0 ehp=80 roll=8/0 desc="" + +=== duo/glass falls early seed=42 === +result: won=false timedOut=false rounds=6 survivors=false + enemy: start=172 entry=172 end=101 + seat[0]: start=160 entry=160 end=0 + seat[1]: start=12 entry=12 end=0 + [00] r1 seat=1 Opening player hit dmg=11 php=12 ehp=161 roll=13/0 desc="" + [01] r1 seat=0 Opening player hit dmg=9 php=160 ehp=152 roll=5/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=16 php=144 ehp=152 roll=15/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=23 php=0 ehp=152 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=18 php=126 ehp=152 roll=13/0 desc="" + [05] r2 seat=0 Clash enemy hit dmg=22 php=104 ehp=152 roll=2/0 desc="" + [06] r2 seat=0 Clash player block dmg=7 php=104 ehp=145 roll=9/0 desc="" + [07] r3 seat=0 Clash player hit dmg=11 php=104 ehp=134 roll=16/0 desc="" + [08] r3 seat=0 Clash enemy crit dmg=48 php=56 ehp=134 roll=20/0 desc="" + [09] r4 seat=0 Clash player hit dmg=14 php=56 ehp=120 roll=5/0 desc="" + [10] r4 seat=0 Clash enemy hit dmg=18 php=38 ehp=120 roll=13/0 desc="" + [11] r5 seat=0 Decisive player block dmg=6 php=38 ehp=114 roll=14/0 desc="" + [12] r5 seat=0 Decisive enemy hit dmg=20 php=18 ehp=114 roll=4/0 desc="" + [13] r6 seat=0 Sudden Death player hit dmg=13 php=18 ehp=101 roll=9/0 desc="" + [14] r6 seat=0 Sudden Death enemy crit dmg=58 php=0 ehp=101 roll=20/0 desc="" + +=== trio/even seed=1 === +result: won=true timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=10 + seat[0]: start=160 entry=160 end=58 + seat[1]: start=90 entry=90 end=0 + seat[2]: start=110 entry=110 end=0 + [00] r1 seat=1 Opening player hit dmg=12 php=90 ehp=333 roll=11/0 desc="" + [01] r1 seat=0 Opening player hit dmg=8 php=160 ehp=325 roll=3/0 desc="" + [02] r1 seat=2 Opening enemy hit dmg=6 php=104 ehp=325 roll=11/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=6 php=84 ehp=325 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=325 roll=19/0 desc="" + [05] r1 seat=0 Opening enemy hit dmg=5 php=149 ehp=325 roll=15/0 desc="" + [06] r1 seat=1 Opening enemy hit dmg=6 php=78 ehp=325 roll=5/0 desc="" + [07] r1 seat=2 Opening player hit dmg=6 php=104 ehp=319 roll=4/0 desc="" + [08] r2 seat=1 Clash player hit dmg=15 php=78 ehp=304 roll=4/0 desc="" + [09] r2 seat=0 Clash player hit dmg=12 php=149 ehp=292 roll=6/0 desc="" + [10] r2 seat=2 Clash player hit dmg=7 php=104 ehp=285 roll=15/0 desc="" + [11] r2 seat=2 Clash enemy hit dmg=7 php=97 ehp=285 roll=13/0 desc="" + [12] r2 seat=0 Clash enemy hit dmg=7 php=142 ehp=285 roll=8/0 desc="" + [13] r2 seat=0 Clash enemy hit dmg=6 php=136 ehp=285 roll=11/0 desc="" + [14] r2 seat=1 Clash enemy miss dmg=0 php=78 ehp=285 roll=1/0 desc="fumble" + [15] r2 seat=2 Clash enemy hit dmg=8 php=89 ehp=285 roll=7/0 desc="" + [16] r3 seat=1 Clash player hit dmg=18 php=78 ehp=267 roll=16/0 desc="" + [17] r3 seat=2 Clash player hit dmg=8 php=89 ehp=259 roll=11/0 desc="" + [18] r3 seat=0 Clash player hit dmg=9 php=136 ehp=250 roll=8/0 desc="" + [19] r3 seat=2 Clash enemy hit dmg=8 php=81 ehp=250 roll=9/0 desc="" + [20] r3 seat=1 Clash enemy hit dmg=8 php=70 ehp=250 roll=16/0 desc="" + [21] r3 seat=0 Clash enemy hit dmg=7 php=129 ehp=250 roll=3/0 desc="" + [22] r3 seat=1 Clash enemy hit dmg=8 php=62 ehp=250 roll=19/0 desc="" + [23] r3 seat=0 Clash enemy hit dmg=8 php=121 ehp=250 roll=12/0 desc="" + [24] r3 seat=0 Clash environment environmental dmg=3 php=118 ehp=250 roll=0/0 desc="" + [25] r4 seat=1 Clash player hit dmg=17 php=62 ehp=233 roll=14/0 desc="" + [26] r4 seat=2 Clash player hit dmg=8 php=81 ehp=225 roll=10/0 desc="" + [27] r4 seat=0 Clash player hit dmg=10 php=118 ehp=215 roll=11/0 desc="" + [28] r4 seat=1 Clash enemy miss dmg=0 php=62 ehp=215 roll=1/0 desc="fumble" + [29] r4 seat=0 Clash enemy hit dmg=7 php=111 ehp=215 roll=14/0 desc="" + [30] r4 seat=2 Clash enemy hit dmg=8 php=73 ehp=215 roll=13/0 desc="" + [31] r4 seat=2 Clash enemy hit dmg=7 php=66 ehp=215 roll=15/0 desc="" + [32] r4 seat=0 Clash enemy crit dmg=12 php=99 ehp=215 roll=20/0 desc="" + [33] r5 seat=1 Decisive player hit dmg=20 php=62 ehp=195 roll=13/0 desc="" + [34] r5 seat=0 Decisive enemy hit dmg=7 php=92 ehp=195 roll=9/0 desc="" + [35] r5 seat=2 Decisive enemy hit dmg=9 php=57 ehp=195 roll=5/0 desc="" + [36] r5 seat=0 Decisive enemy hit dmg=9 php=83 ehp=195 roll=12/0 desc="" + [37] r5 seat=0 Decisive enemy hit dmg=7 php=76 ehp=195 roll=17/0 desc="" + [38] r5 seat=2 Decisive enemy hit dmg=9 php=48 ehp=195 roll=7/0 desc="" + [39] r5 seat=2 Decisive player hit dmg=8 php=48 ehp=187 roll=8/0 desc="" + [40] r5 seat=0 Decisive player hit dmg=11 php=76 ehp=176 roll=10/0 desc="" + [41] r6 seat=1 Sudden Death player hit dmg=19 php=62 ehp=157 roll=18/0 desc="" + [42] r6 seat=0 Sudden Death player hit dmg=14 php=76 ehp=143 roll=18/0 desc="" + [43] r6 seat=1 Sudden Death enemy hit dmg=9 php=53 ehp=143 roll=13/0 desc="" + [44] r6 seat=0 Sudden Death enemy miss dmg=0 php=76 ehp=143 roll=1/0 desc="fumble" + [45] r6 seat=1 Sudden Death enemy hit dmg=9 php=44 ehp=143 roll=2/0 desc="" + [46] r6 seat=1 Sudden Death enemy hit dmg=10 php=34 ehp=143 roll=2/0 desc="" + [47] r6 seat=1 Sudden Death enemy hit dmg=11 php=23 ehp=143 roll=11/0 desc="" + [48] r6 seat=2 Sudden Death player hit dmg=11 php=48 ehp=132 roll=11/0 desc="" + [49] r7 seat=1 Sudden Death player hit dmg=18 php=23 ehp=114 roll=3/0 desc="" + [50] r7 seat=2 Sudden Death player hit dmg=9 php=48 ehp=105 roll=4/0 desc="" + [51] r7 seat=0 Sudden Death player hit dmg=14 php=76 ehp=91 roll=15/0 desc="" + [52] r7 seat=2 Sudden Death enemy hit dmg=9 php=39 ehp=91 roll=11/0 desc="" + [53] r7 seat=2 Sudden Death enemy crit dmg=20 php=19 ehp=91 roll=20/0 desc="" + [54] r7 seat=1 Sudden Death enemy hit dmg=11 php=12 ehp=91 roll=5/0 desc="" + [55] r7 seat=2 Sudden Death enemy hit dmg=9 php=10 ehp=91 roll=5/0 desc="" + [56] r7 seat=2 Sudden Death enemy hit dmg=10 php=0 ehp=91 roll=2/0 desc="" + [57] r8 seat=1 Sudden Death player hit dmg=19 php=12 ehp=72 roll=19/0 desc="" + [58] r8 seat=0 Sudden Death enemy hit dmg=9 php=67 ehp=72 roll=13/0 desc="" + [59] r8 seat=1 Sudden Death enemy miss dmg=0 php=12 ehp=72 roll=1/0 desc="fumble" + [60] r8 seat=0 Sudden Death player hit dmg=15 php=67 ehp=57 roll=6/0 desc="" + [61] r9 seat=1 Sudden Death player hit dmg=23 php=12 ehp=34 roll=3/0 desc="" + [62] r9 seat=1 Sudden Death enemy hit dmg=11 php=1 ehp=34 roll=14/0 desc="" + [63] r9 seat=1 Sudden Death enemy hit dmg=9 php=0 ehp=34 roll=17/0 desc="" + [64] r9 seat=0 Sudden Death player hit dmg=12 php=67 ehp=22 roll=4/0 desc="" + [65] r10 seat=0 Sudden Death player hit dmg=12 php=67 ehp=10 roll=9/0 desc="" + [66] r10 seat=0 Sudden Death enemy hit dmg=9 php=58 ehp=10 roll=9/0 desc="" + [67] r10 seat=0 exhaust system timeout dmg=0 php=58 ehp=10 roll=0/0 desc="" + +=== trio/even seed=2 === +result: won=false timedOut=true rounds=8 survivors=true + enemy: start=345 entry=345 end=76 + seat[0]: start=160 entry=160 end=71 + seat[1]: start=90 entry=90 end=0 + seat[2]: start=110 entry=110 end=1 + [00] r1 seat=1 Opening player hit dmg=15 php=90 ehp=330 roll=16/0 desc="" + [01] r1 seat=2 Opening player hit dmg=7 php=110 ehp=323 roll=7/0 desc="" + [02] r1 seat=0 Opening enemy crit dmg=14 php=146 ehp=323 roll=20/0 desc="" + [03] r1 seat=0 Opening enemy hit dmg=6 php=140 ehp=323 roll=12/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=7 php=133 ehp=323 roll=18/0 desc="" + [05] r1 seat=2 Opening enemy hit dmg=6 php=104 ehp=323 roll=7/0 desc="" + [06] r1 seat=2 Opening enemy hit dmg=7 php=97 ehp=323 roll=2/0 desc="" + [07] r1 seat=0 Opening player hit dmg=10 php=133 ehp=313 roll=15/0 desc="" + [08] r2 seat=1 Clash player hit dmg=17 php=90 ehp=296 roll=3/0 desc="" + [09] r2 seat=2 Clash player hit dmg=9 php=97 ehp=287 roll=16/0 desc="" + [10] r2 seat=0 Clash enemy hit dmg=7 php=126 ehp=287 roll=12/0 desc="" + [11] r2 seat=1 Clash enemy hit dmg=9 php=81 ehp=287 roll=13/0 desc="" + [12] r2 seat=0 Clash enemy hit dmg=7 php=119 ehp=287 roll=5/0 desc="" + [13] r2 seat=2 Clash enemy hit dmg=7 php=90 ehp=287 roll=17/0 desc="" + [14] r2 seat=0 Clash enemy hit dmg=7 php=112 ehp=287 roll=19/0 desc="" + [15] r2 seat=0 Clash player hit dmg=12 php=112 ehp=275 roll=2/0 desc="" + [16] r3 seat=2 Clash player hit dmg=7 php=90 ehp=268 roll=9/0 desc="" + [17] r3 seat=1 Clash player hit dmg=15 php=81 ehp=253 roll=15/0 desc="" + [18] r3 seat=1 Clash enemy hit dmg=8 php=73 ehp=253 roll=2/0 desc="" + [19] r3 seat=0 Clash enemy hit dmg=7 php=105 ehp=253 roll=7/0 desc="" + [20] r3 seat=1 Clash enemy hit dmg=8 php=65 ehp=253 roll=3/0 desc="" + [21] r3 seat=1 Clash enemy hit dmg=9 php=56 ehp=253 roll=8/0 desc="" + [22] r3 seat=2 Clash enemy hit dmg=8 php=82 ehp=253 roll=18/0 desc="" + [23] r3 seat=0 Clash player hit dmg=11 php=105 ehp=242 roll=19/0 desc="" + [24] r3 seat=2 Clash environment environmental dmg=5 php=77 ehp=242 roll=0/0 desc="" + [25] r4 seat=2 Decisive player hit dmg=8 php=77 ehp=234 roll=18/0 desc="" + [26] r4 seat=1 Decisive player hit dmg=16 php=56 ehp=218 roll=14/0 desc="" + [27] r4 seat=0 Decisive player hit dmg=12 php=105 ehp=206 roll=8/0 desc="" + [28] r4 seat=0 Decisive enemy hit dmg=7 php=98 ehp=206 roll=11/0 desc="" + [29] r4 seat=2 Decisive enemy hit dmg=9 php=68 ehp=206 roll=10/0 desc="" + [30] r4 seat=2 Decisive enemy block dmg=3 php=65 ehp=206 roll=15/0 desc="" + [31] r4 seat=2 Decisive enemy hit dmg=8 php=57 ehp=206 roll=11/0 desc="" + [32] r4 seat=1 Decisive enemy hit dmg=8 php=48 ehp=206 roll=2/0 desc="" + [33] r5 seat=2 Sudden Death player hit dmg=10 php=57 ehp=196 roll=7/0 desc="" + [34] r5 seat=1 Sudden Death player hit dmg=19 php=48 ehp=177 roll=19/0 desc="" + [35] r5 seat=0 Sudden Death player hit dmg=14 php=98 ehp=163 roll=15/0 desc="" + [36] r5 seat=1 Sudden Death enemy hit dmg=11 php=37 ehp=163 roll=10/0 desc="" + [37] r5 seat=1 Sudden Death enemy hit dmg=11 php=26 ehp=163 roll=5/0 desc="" + [38] r5 seat=1 Sudden Death enemy hit dmg=10 php=16 ehp=163 roll=7/0 desc="" + [39] r5 seat=2 Sudden Death enemy hit dmg=8 php=49 ehp=163 roll=5/0 desc="" + [40] r5 seat=2 Sudden Death enemy hit dmg=10 php=39 ehp=163 roll=6/0 desc="" + [41] r6 seat=1 Sudden Death player hit dmg=20 php=16 ehp=143 roll=4/0 desc="" + [42] r6 seat=2 Sudden Death player hit dmg=11 php=39 ehp=132 roll=12/0 desc="" + [43] r6 seat=0 Sudden Death player hit dmg=11 php=98 ehp=121 roll=12/0 desc="" + [44] r6 seat=1 Sudden Death enemy hit dmg=10 php=6 ehp=121 roll=10/0 desc="" + [45] r6 seat=1 Sudden Death enemy hit dmg=8 php=0 ehp=121 roll=10/0 desc="" + [46] r6 seat=2 Sudden Death enemy block dmg=4 php=35 ehp=121 roll=13/0 desc="" + [47] r6 seat=0 Sudden Death enemy hit dmg=9 php=89 ehp=121 roll=14/0 desc="" + [48] r6 seat=0 Sudden Death enemy hit dmg=8 php=81 ehp=121 roll=6/0 desc="" + [49] r7 seat=2 Sudden Death player hit dmg=10 php=35 ehp=111 roll=7/0 desc="" + [50] r7 seat=0 Sudden Death player hit dmg=11 php=81 ehp=100 roll=4/0 desc="" + [51] r7 seat=2 Sudden Death enemy hit dmg=8 php=27 ehp=100 roll=2/0 desc="" + [52] r7 seat=0 Sudden Death enemy hit dmg=10 php=71 ehp=100 roll=13/0 desc="" + [53] r8 seat=2 Sudden Death player hit dmg=11 php=27 ehp=89 roll=18/0 desc="" + [54] r8 seat=0 Sudden Death player hit dmg=13 php=71 ehp=76 roll=4/0 desc="" + [55] r8 seat=2 Sudden Death enemy hit dmg=9 php=18 ehp=76 roll=17/0 desc="" + [56] r8 seat=2 Sudden Death enemy hit dmg=9 php=9 ehp=76 roll=12/0 desc="" + [57] r8 seat=2 Sudden Death enemy hit dmg=8 php=1 ehp=76 roll=19/0 desc="" + [58] r8 seat=0 exhaust system timeout dmg=0 php=71 ehp=76 roll=0/0 desc="" + +=== trio/even seed=3 === +result: won=true timedOut=false rounds=10 survivors=true + enemy: start=345 entry=345 end=0 + seat[0]: start=160 entry=160 end=32 + seat[1]: start=90 entry=90 end=0 + seat[2]: start=110 entry=110 end=0 + [00] r1 seat=1 Opening player hit dmg=15 php=90 ehp=330 roll=8/0 desc="" + [01] r1 seat=2 Opening player hit dmg=6 php=110 ehp=324 roll=18/0 desc="" + [02] r1 seat=0 Opening player hit dmg=9 php=160 ehp=315 roll=8/0 desc="" + [03] r1 seat=0 Opening enemy hit dmg=5 php=155 ehp=315 roll=16/0 desc="" + [04] r1 seat=1 Opening enemy hit dmg=7 php=83 ehp=315 roll=8/0 desc="" + [05] r1 seat=1 Opening enemy hit dmg=6 php=77 ehp=315 roll=15/0 desc="" + [06] r1 seat=0 Opening enemy hit dmg=6 php=149 ehp=315 roll=12/0 desc="" + [07] r1 seat=0 Opening enemy hit dmg=6 php=143 ehp=315 roll=13/0 desc="" + [08] r1 seat=1 Opening environment environmental dmg=6 php=71 ehp=315 roll=0/0 desc="" + [09] r2 seat=1 Clash player hit dmg=15 php=71 ehp=300 roll=4/0 desc="" + [10] r2 seat=2 Clash enemy hit dmg=7 php=103 ehp=300 roll=12/0 desc="" + [11] r2 seat=1 Clash enemy hit dmg=9 php=62 ehp=300 roll=14/0 desc="" + [12] r2 seat=1 Clash enemy block dmg=3 php=59 ehp=300 roll=17/0 desc="" + [13] r2 seat=0 Clash enemy hit dmg=8 php=135 ehp=300 roll=12/0 desc="" + [14] r2 seat=2 Clash enemy miss dmg=0 php=103 ehp=300 roll=1/0 desc="fumble" + [15] r2 seat=0 Clash player hit dmg=12 php=135 ehp=288 roll=7/0 desc="" + [16] r2 seat=2 Clash player hit dmg=7 php=103 ehp=281 roll=8/0 desc="" + [17] r3 seat=2 Clash player hit dmg=8 php=103 ehp=273 roll=19/0 desc="" + [18] r3 seat=1 Clash player hit dmg=19 php=59 ehp=254 roll=18/0 desc="" + [19] r3 seat=0 Clash player hit dmg=12 php=135 ehp=242 roll=11/0 desc="" + [20] r3 seat=2 Clash enemy hit dmg=8 php=95 ehp=242 roll=5/0 desc="" + [21] r3 seat=2 Clash enemy hit dmg=7 php=88 ehp=242 roll=7/0 desc="" + [22] r3 seat=0 Clash enemy hit dmg=8 php=127 ehp=242 roll=4/0 desc="" + [23] r3 seat=2 Clash enemy hit dmg=7 php=81 ehp=242 roll=16/0 desc="" + [24] r3 seat=2 Clash enemy hit dmg=7 php=74 ehp=242 roll=8/0 desc="" + [25] r3 seat=0 Clash environment environmental dmg=3 php=124 ehp=242 roll=0/0 desc="" + [26] r4 seat=1 Clash player hit dmg=17 php=59 ehp=225 roll=5/0 desc="" + [27] r4 seat=0 Clash player hit dmg=10 php=124 ehp=215 roll=14/0 desc="" + [28] r4 seat=2 Clash enemy hit dmg=7 php=67 ehp=215 roll=10/0 desc="" + [29] r4 seat=2 Clash enemy hit dmg=8 php=59 ehp=215 roll=4/0 desc="" + [30] r4 seat=1 Clash enemy hit dmg=9 php=50 ehp=215 roll=4/0 desc="" + [31] r4 seat=0 Clash enemy hit dmg=8 php=116 ehp=215 roll=12/0 desc="" + [32] r4 seat=0 Clash enemy hit dmg=8 php=108 ehp=215 roll=11/0 desc="" + [33] r4 seat=2 Clash player hit dmg=9 php=59 ehp=206 roll=3/0 desc="" + [34] r5 seat=1 Decisive player hit dmg=20 php=50 ehp=186 roll=5/0 desc="" + [35] r5 seat=2 Decisive player hit dmg=8 php=59 ehp=178 roll=10/0 desc="" + [36] r5 seat=0 Decisive player hit dmg=11 php=108 ehp=167 roll=17/0 desc="" + [37] r5 seat=2 Decisive enemy hit dmg=9 php=50 ehp=167 roll=17/0 desc="" + [38] r5 seat=2 Decisive enemy hit dmg=9 php=41 ehp=167 roll=13/0 desc="" + [39] r5 seat=2 Decisive enemy hit dmg=8 php=33 ehp=167 roll=8/0 desc="" + [40] r5 seat=1 Decisive enemy hit dmg=7 php=43 ehp=167 roll=6/0 desc="" + [41] r5 seat=0 Decisive enemy hit dmg=7 php=101 ehp=167 roll=16/0 desc="" + [42] r6 seat=1 Sudden Death player crit dmg=44 php=43 ehp=123 roll=20/0 desc="" + [43] r6 seat=2 Sudden Death player hit dmg=11 php=33 ehp=112 roll=14/0 desc="" + [44] r6 seat=1 Sudden Death enemy hit dmg=8 php=35 ehp=112 roll=11/0 desc="" + [45] r6 seat=1 Sudden Death enemy hit dmg=10 php=25 ehp=112 roll=9/0 desc="" + [46] r6 seat=0 Sudden Death enemy hit dmg=10 php=91 ehp=112 roll=5/0 desc="" + [47] r6 seat=0 Sudden Death enemy hit dmg=10 php=81 ehp=112 roll=14/0 desc="" + [48] r6 seat=0 Sudden Death enemy hit dmg=8 php=73 ehp=112 roll=7/0 desc="" + [49] r6 seat=0 Sudden Death player hit dmg=11 php=73 ehp=101 roll=11/0 desc="" + [50] r6 seat=1 Sudden Death environment environmental dmg=4 php=21 ehp=101 roll=0/0 desc="" + [51] r7 seat=1 Sudden Death player hit dmg=20 php=21 ehp=81 roll=15/0 desc="" + [52] r7 seat=0 Sudden Death player hit dmg=15 php=73 ehp=66 roll=7/0 desc="" + [53] r7 seat=2 Sudden Death player hit dmg=9 php=33 ehp=57 roll=4/0 desc="" + [54] r7 seat=1 Sudden Death enemy hit dmg=9 php=12 ehp=57 roll=5/0 desc="" + [55] r7 seat=1 Sudden Death enemy hit dmg=9 php=3 ehp=57 roll=15/0 desc="" + [56] r7 seat=2 Sudden Death enemy hit dmg=10 php=23 ehp=57 roll=14/0 desc="" + [57] r7 seat=1 Sudden Death enemy hit dmg=11 php=0 ehp=57 roll=19/0 desc="" + [58] r7 seat=2 Sudden Death enemy hit dmg=9 php=14 ehp=57 roll=15/0 desc="" + [59] r8 seat=2 Sudden Death player hit dmg=11 php=14 ehp=46 roll=14/0 desc="" + [60] r8 seat=0 Sudden Death player hit dmg=11 php=73 ehp=35 roll=17/0 desc="" + [61] r8 seat=0 Sudden Death enemy hit dmg=8 php=65 ehp=35 roll=15/0 desc="" + [62] r8 seat=2 Sudden Death enemy hit dmg=8 php=6 ehp=35 roll=16/0 desc="" + [63] r8 seat=0 Sudden Death enemy block dmg=4 php=61 ehp=35 roll=9/0 desc="" + [64] r9 seat=2 Sudden Death player hit dmg=9 php=6 ehp=26 roll=16/0 desc="" + [65] r9 seat=0 Sudden Death player hit dmg=14 php=61 ehp=12 roll=6/0 desc="" + [66] r9 seat=0 Sudden Death enemy hit dmg=9 php=52 ehp=12 roll=3/0 desc="" + [67] r9 seat=2 Sudden Death enemy hit dmg=8 php=0 ehp=12 roll=5/0 desc="" + [68] r9 seat=0 Sudden Death enemy hit dmg=10 php=42 ehp=12 roll=10/0 desc="" + [69] r10 seat=0 Sudden Death enemy hit dmg=10 php=32 ehp=12 roll=5/0 desc="" + [70] r10 seat=0 Sudden Death player crit dmg=26 php=32 ehp=0 roll=20/0 desc="" + +=== trio/even seed=7 === +result: won=true timedOut=false rounds=10 survivors=true + enemy: start=345 entry=345 end=0 + seat[0]: start=160 entry=160 end=9 + seat[1]: start=90 entry=90 end=12 + seat[2]: start=110 entry=110 end=0 + [00] r1 seat=1 Opening player hit dmg=13 php=90 ehp=332 roll=6/0 desc="" + [01] r1 seat=2 Opening player hit dmg=7 php=110 ehp=325 roll=16/0 desc="" + [02] r1 seat=0 Opening player hit dmg=8 php=160 ehp=317 roll=16/0 desc="" + [03] r1 seat=2 Opening enemy block dmg=3 php=107 ehp=317 roll=11/0 desc="" + [04] r1 seat=2 Opening enemy hit dmg=5 php=102 ehp=317 roll=14/0 desc="" + [05] r1 seat=2 Opening enemy hit dmg=7 php=95 ehp=317 roll=13/0 desc="" + [06] r1 seat=2 Opening enemy hit dmg=7 php=88 ehp=317 roll=18/0 desc="" + [07] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=317 roll=9/0 desc="" + [08] r1 seat=1 Opening environment environmental dmg=3 php=87 ehp=317 roll=0/0 desc="" + [09] r2 seat=1 Clash player hit dmg=15 php=87 ehp=302 roll=17/0 desc="" + [10] r2 seat=2 Clash player hit dmg=7 php=88 ehp=295 roll=8/0 desc="" + [11] r2 seat=1 Clash enemy hit dmg=9 php=78 ehp=295 roll=9/0 desc="" + [12] r2 seat=0 Clash enemy hit dmg=7 php=147 ehp=295 roll=10/0 desc="" + [13] r2 seat=0 Clash enemy hit dmg=7 php=140 ehp=295 roll=6/0 desc="" + [14] r2 seat=1 Clash enemy hit dmg=8 php=70 ehp=295 roll=16/0 desc="" + [15] r2 seat=0 Clash enemy hit dmg=8 php=132 ehp=295 roll=11/0 desc="" + [16] r2 seat=0 Clash player hit dmg=10 php=132 ehp=285 roll=18/0 desc="" + [17] r3 seat=2 Clash player hit dmg=9 php=88 ehp=276 roll=13/0 desc="" + [18] r3 seat=2 Clash enemy hit dmg=7 php=81 ehp=276 roll=8/0 desc="" + [19] r3 seat=1 Clash enemy hit dmg=7 php=63 ehp=276 roll=8/0 desc="" + [20] r3 seat=2 Clash enemy hit dmg=7 php=74 ehp=276 roll=10/0 desc="" + [21] r3 seat=0 Clash enemy hit dmg=8 php=124 ehp=276 roll=3/0 desc="" + [22] r3 seat=2 Clash enemy hit dmg=7 php=67 ehp=276 roll=10/0 desc="" + [23] r3 seat=0 Clash player hit dmg=11 php=124 ehp=265 roll=4/0 desc="" + [24] r3 seat=1 Clash player hit dmg=15 php=63 ehp=250 roll=6/0 desc="" + [25] r4 seat=1 Clash player crit dmg=30 php=63 ehp=220 roll=20/0 desc="" + [26] r4 seat=2 Clash player hit dmg=9 php=67 ehp=211 roll=13/0 desc="" + [27] r4 seat=1 Clash enemy crit dmg=14 php=49 ehp=211 roll=20/0 desc="" + [28] r4 seat=2 Clash enemy hit dmg=7 php=60 ehp=211 roll=2/0 desc="" + [29] r4 seat=2 Clash enemy miss dmg=0 php=60 ehp=211 roll=1/0 desc="fumble" + [30] r4 seat=0 Clash enemy hit dmg=8 php=116 ehp=211 roll=10/0 desc="" + [31] r4 seat=2 Clash enemy hit dmg=7 php=53 ehp=211 roll=17/0 desc="" + [32] r4 seat=0 Clash player hit dmg=9 php=116 ehp=202 roll=8/0 desc="" + [33] r5 seat=1 Decisive player hit dmg=16 php=49 ehp=186 roll=18/0 desc="" + [34] r5 seat=0 Decisive player hit dmg=13 php=116 ehp=173 roll=4/0 desc="" + [35] r5 seat=2 Decisive player hit dmg=9 php=53 ehp=164 roll=8/0 desc="" + [36] r5 seat=0 Decisive enemy hit dmg=9 php=107 ehp=164 roll=3/0 desc="" + [37] r5 seat=1 Decisive enemy hit dmg=7 php=42 ehp=164 roll=14/0 desc="" + [38] r5 seat=2 Decisive enemy hit dmg=7 php=46 ehp=164 roll=17/0 desc="" + [39] r5 seat=2 Decisive enemy block dmg=3 php=43 ehp=164 roll=7/0 desc="" + [40] r5 seat=2 Decisive enemy hit dmg=9 php=34 ehp=164 roll=17/0 desc="" + [41] r6 seat=2 Sudden Death player hit dmg=9 php=34 ehp=155 roll=13/0 desc="" + [42] r6 seat=1 Sudden Death player hit dmg=21 php=42 ehp=134 roll=5/0 desc="" + [43] r6 seat=0 Sudden Death enemy hit dmg=10 php=97 ehp=134 roll=12/0 desc="" + [44] r6 seat=0 Sudden Death enemy hit dmg=8 php=89 ehp=134 roll=6/0 desc="" + [45] r6 seat=0 Sudden Death enemy block dmg=4 php=85 ehp=134 roll=2/0 desc="" + [46] r6 seat=0 Sudden Death enemy hit dmg=8 php=77 ehp=134 roll=19/0 desc="" + [47] r6 seat=2 Sudden Death enemy hit dmg=8 php=26 ehp=134 roll=9/0 desc="" + [48] r6 seat=0 Sudden Death player hit dmg=13 php=77 ehp=121 roll=2/0 desc="" + [49] r7 seat=1 Sudden Death player hit dmg=22 php=42 ehp=99 roll=16/0 desc="" + [50] r7 seat=0 Sudden Death player hit dmg=12 php=77 ehp=87 roll=6/0 desc="" + [51] r7 seat=2 Sudden Death player hit dmg=11 php=26 ehp=76 roll=8/0 desc="" + [52] r7 seat=2 Sudden Death enemy hit dmg=8 php=18 ehp=76 roll=4/0 desc="" + [53] r7 seat=0 Sudden Death enemy hit dmg=10 php=67 ehp=76 roll=11/0 desc="" + [54] r7 seat=0 Sudden Death enemy hit dmg=10 php=57 ehp=76 roll=2/0 desc="" + [55] r7 seat=2 Sudden Death enemy crit dmg=16 php=2 ehp=76 roll=20/0 desc="" + [56] r7 seat=0 Sudden Death enemy hit dmg=10 php=47 ehp=76 roll=11/0 desc="" + [57] r8 seat=1 Sudden Death player hit dmg=19 php=42 ehp=57 roll=7/0 desc="" + [58] r8 seat=2 Sudden Death player hit dmg=11 php=2 ehp=46 roll=8/0 desc="" + [59] r8 seat=1 Sudden Death enemy hit dmg=9 php=33 ehp=46 roll=4/0 desc="" + [60] r8 seat=0 Sudden Death enemy hit dmg=10 php=37 ehp=46 roll=14/0 desc="" + [61] r8 seat=1 Sudden Death enemy hit dmg=10 php=23 ehp=46 roll=12/0 desc="" + [62] r8 seat=2 Sudden Death enemy hit dmg=10 php=0 ehp=46 roll=5/0 desc="" + [63] r8 seat=0 Sudden Death enemy hit dmg=9 php=28 ehp=46 roll=15/0 desc="" + [64] r8 seat=0 Sudden Death player hit dmg=12 php=28 ehp=34 roll=14/0 desc="" + [65] r9 seat=1 Sudden Death player hit dmg=23 php=23 ehp=11 roll=2/0 desc="" + [66] r9 seat=1 Sudden Death enemy hit dmg=11 php=12 ehp=11 roll=7/0 desc="" + [67] r9 seat=0 Sudden Death enemy hit dmg=10 php=18 ehp=11 roll=13/0 desc="" + [68] r9 seat=0 Sudden Death enemy hit dmg=9 php=9 ehp=11 roll=10/0 desc="" + [69] r9 seat=0 Sudden Death player miss dmg=0 php=9 ehp=11 roll=1/0 desc="fumble" + [70] r10 seat=1 Sudden Death player hit dmg=24 php=12 ehp=0 roll=5/0 desc="" + +=== trio/even seed=42 === +result: won=true timedOut=true rounds=8 survivors=true + enemy: start=345 entry=345 end=20 + seat[0]: start=160 entry=160 end=63 + seat[1]: start=90 entry=90 end=0 + seat[2]: start=110 entry=110 end=5 + [00] r1 seat=1 Opening player miss dmg=0 php=90 ehp=345 roll=1/0 desc="fumble" + [01] r1 seat=0 Opening player hit dmg=8 php=160 ehp=337 roll=17/0 desc="" + [02] r1 seat=2 Opening player hit dmg=6 php=110 ehp=331 roll=19/0 desc="" + [03] r1 seat=2 Opening enemy hit dmg=7 php=103 ehp=331 roll=18/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=7 php=153 ehp=331 roll=19/0 desc="" + [05] r1 seat=1 Opening enemy hit dmg=7 php=83 ehp=331 roll=2/0 desc="" + [06] r1 seat=1 Opening enemy hit dmg=7 php=76 ehp=331 roll=2/0 desc="" + [07] r1 seat=0 Opening enemy hit dmg=6 php=147 ehp=331 roll=2/0 desc="" + [08] r1 seat=2 Opening environment environmental dmg=6 php=97 ehp=331 roll=0/0 desc="" + [09] r2 seat=1 Clash player hit dmg=17 php=76 ehp=314 roll=10/0 desc="" + [10] r2 seat=0 Clash player hit dmg=10 php=147 ehp=304 roll=15/0 desc="" + [11] r2 seat=2 Clash player hit dmg=10 php=97 ehp=294 roll=16/0 desc="" + [12] r2 seat=0 Clash enemy block dmg=4 php=143 ehp=294 roll=5/0 desc="" + [13] r2 seat=1 Clash enemy hit dmg=7 php=69 ehp=294 roll=6/0 desc="" + [14] r2 seat=0 Clash enemy miss dmg=0 php=143 ehp=294 roll=1/0 desc="fumble" + [15] r2 seat=2 Clash enemy hit dmg=9 php=88 ehp=294 roll=15/0 desc="" + [16] r2 seat=1 Clash enemy hit dmg=7 php=62 ehp=294 roll=4/0 desc="" + [17] r3 seat=1 Clash player hit dmg=19 php=62 ehp=275 roll=6/0 desc="" + [18] r3 seat=0 Clash player hit dmg=10 php=143 ehp=265 roll=4/0 desc="" + [19] r3 seat=2 Clash player hit dmg=8 php=88 ehp=257 roll=15/0 desc="" + [20] r3 seat=2 Clash enemy hit dmg=7 php=81 ehp=257 roll=5/0 desc="" + [21] r3 seat=2 Clash enemy hit dmg=8 php=73 ehp=257 roll=10/0 desc="" + [22] r3 seat=1 Clash enemy miss dmg=0 php=62 ehp=257 roll=1/0 desc="fumble" + [23] r3 seat=0 Clash enemy hit dmg=7 php=136 ehp=257 roll=17/0 desc="" + [24] r3 seat=2 Clash enemy hit dmg=7 php=66 ehp=257 roll=15/0 desc="" + [25] r4 seat=2 Decisive player hit dmg=10 php=66 ehp=247 roll=4/0 desc="" + [26] r4 seat=1 Decisive player hit dmg=17 php=62 ehp=230 roll=17/0 desc="" + [27] r4 seat=0 Decisive enemy block dmg=3 php=133 ehp=230 roll=4/0 desc="" + [28] r4 seat=0 Decisive enemy hit dmg=7 php=126 ehp=230 roll=15/0 desc="" + [29] r4 seat=0 Decisive enemy hit dmg=8 php=118 ehp=230 roll=3/0 desc="" + [30] r4 seat=2 Decisive enemy hit dmg=8 php=58 ehp=230 roll=13/0 desc="" + [31] r4 seat=0 Decisive enemy hit dmg=8 php=110 ehp=230 roll=7/0 desc="" + [32] r4 seat=0 Decisive player hit dmg=10 php=110 ehp=220 roll=7/0 desc="" + [33] r5 seat=2 Sudden Death player hit dmg=10 php=58 ehp=210 roll=13/0 desc="" + [34] r5 seat=1 Sudden Death player hit dmg=18 php=62 ehp=192 roll=15/0 desc="" + [35] r5 seat=0 Sudden Death player hit dmg=12 php=110 ehp=180 roll=7/0 desc="" + [36] r5 seat=0 Sudden Death enemy hit dmg=8 php=102 ehp=180 roll=8/0 desc="" + [37] r5 seat=0 Sudden Death enemy hit dmg=8 php=94 ehp=180 roll=4/0 desc="" + [38] r5 seat=2 Sudden Death enemy miss dmg=0 php=58 ehp=180 roll=1/0 desc="fumble" + [39] r5 seat=1 Sudden Death enemy hit dmg=9 php=53 ehp=180 roll=3/0 desc="" + [40] r5 seat=2 Sudden Death enemy hit dmg=10 php=48 ehp=180 roll=16/0 desc="" + [41] r6 seat=1 Sudden Death player hit dmg=19 php=53 ehp=161 roll=16/0 desc="" + [42] r6 seat=0 Sudden Death player hit dmg=14 php=94 ehp=147 roll=6/0 desc="" + [43] r6 seat=2 Sudden Death player hit dmg=10 php=48 ehp=137 roll=7/0 desc="" + [44] r6 seat=1 Sudden Death enemy block dmg=5 php=48 ehp=137 roll=11/0 desc="" + [45] r6 seat=1 Sudden Death enemy hit dmg=11 php=37 ehp=137 roll=7/0 desc="" + [46] r6 seat=0 Sudden Death enemy block dmg=4 php=90 ehp=137 roll=4/0 desc="" + [47] r6 seat=2 Sudden Death enemy hit dmg=9 php=39 ehp=137 roll=4/0 desc="" + [48] r6 seat=0 Sudden Death enemy hit dmg=9 php=81 ehp=137 roll=9/0 desc="" + [49] r7 seat=1 Sudden Death player crit dmg=38 php=37 ehp=99 roll=20/0 desc="" + [50] r7 seat=0 Sudden Death player hit dmg=13 php=81 ehp=86 roll=9/0 desc="" + [51] r7 seat=2 Sudden Death player hit dmg=10 php=39 ehp=76 roll=7/0 desc="" + [52] r7 seat=2 Sudden Death enemy block dmg=5 php=34 ehp=76 roll=11/0 desc="" + [53] r7 seat=1 Sudden Death enemy hit dmg=10 php=27 ehp=76 roll=16/0 desc="" + [54] r7 seat=0 Sudden Death enemy hit dmg=10 php=71 ehp=76 roll=11/0 desc="" + [55] r7 seat=1 Sudden Death enemy hit dmg=9 php=18 ehp=76 roll=15/0 desc="" + [56] r7 seat=1 Sudden Death enemy hit dmg=10 php=8 ehp=76 roll=15/0 desc="" + [57] r8 seat=1 Sudden Death player hit dmg=24 php=8 ehp=52 roll=4/0 desc="" + [58] r8 seat=0 Sudden Death player hit dmg=12 php=71 ehp=40 roll=19/0 desc="" + [59] r8 seat=2 Sudden Death player crit dmg=20 php=34 ehp=20 roll=20/0 desc="" + [60] r8 seat=1 Sudden Death enemy hit dmg=11 php=0 ehp=20 roll=18/0 desc="" + [61] r8 seat=2 Sudden Death enemy hit dmg=10 php=24 ehp=20 roll=17/0 desc="" + [62] r8 seat=2 Sudden Death enemy hit dmg=10 php=14 ehp=20 roll=15/0 desc="" + [63] r8 seat=0 Sudden Death enemy hit dmg=8 php=63 ehp=20 roll=10/0 desc="" + [64] r8 seat=2 Sudden Death enemy hit dmg=9 php=5 ehp=20 roll=17/0 desc="" + [65] r8 seat=0 exhaust system timeout dmg=0 php=63 ehp=20 roll=0/0 desc="" + +=== trio/one weak seat seed=1 === +result: won=true timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=23 + seat[0]: start=160 entry=160 end=33 + seat[1]: start=90 entry=90 end=0 + seat[2]: start=70 entry=70 end=0 + [00] r1 seat=1 Opening player hit dmg=12 php=90 ehp=333 roll=11/0 desc="" + [01] r1 seat=0 Opening player hit dmg=8 php=160 ehp=325 roll=3/0 desc="" + [02] r1 seat=2 Opening enemy hit dmg=7 php=63 ehp=325 roll=11/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=6 php=84 ehp=325 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=325 roll=19/0 desc="" + [05] r1 seat=0 Opening enemy hit dmg=5 php=149 ehp=325 roll=15/0 desc="" + [06] r1 seat=1 Opening enemy hit dmg=6 php=78 ehp=325 roll=5/0 desc="" + [07] r1 seat=2 Opening player hit dmg=4 php=63 ehp=321 roll=4/0 desc="" + [08] r2 seat=1 Clash player hit dmg=15 php=78 ehp=306 roll=4/0 desc="" + [09] r2 seat=0 Clash player hit dmg=12 php=149 ehp=294 roll=6/0 desc="" + [10] r2 seat=2 Clash player hit dmg=5 php=63 ehp=289 roll=15/0 desc="" + [11] r2 seat=2 Clash enemy hit dmg=8 php=55 ehp=289 roll=13/0 desc="" + [12] r2 seat=0 Clash enemy hit dmg=7 php=142 ehp=289 roll=8/0 desc="" + [13] r2 seat=0 Clash enemy hit dmg=6 php=136 ehp=289 roll=11/0 desc="" + [14] r2 seat=1 Clash enemy miss dmg=0 php=78 ehp=289 roll=1/0 desc="fumble" + [15] r2 seat=2 Clash enemy hit dmg=9 php=46 ehp=289 roll=7/0 desc="" + [16] r3 seat=1 Clash player hit dmg=18 php=78 ehp=271 roll=16/0 desc="" + [17] r3 seat=2 Clash player hit dmg=5 php=46 ehp=266 roll=11/0 desc="" + [18] r3 seat=0 Clash player hit dmg=9 php=136 ehp=257 roll=8/0 desc="" + [19] r3 seat=2 Clash enemy hit dmg=9 php=37 ehp=257 roll=9/0 desc="" + [20] r3 seat=1 Clash enemy hit dmg=8 php=70 ehp=257 roll=16/0 desc="" + [21] r3 seat=0 Clash enemy hit dmg=7 php=129 ehp=257 roll=3/0 desc="" + [22] r3 seat=1 Clash enemy hit dmg=8 php=62 ehp=257 roll=19/0 desc="" + [23] r3 seat=0 Clash enemy hit dmg=8 php=121 ehp=257 roll=12/0 desc="" + [24] r3 seat=0 Clash environment environmental dmg=3 php=118 ehp=257 roll=0/0 desc="" + [25] r4 seat=1 Clash player hit dmg=17 php=62 ehp=240 roll=14/0 desc="" + [26] r4 seat=2 Clash player hit dmg=6 php=37 ehp=234 roll=10/0 desc="" + [27] r4 seat=0 Clash player hit dmg=10 php=118 ehp=224 roll=11/0 desc="" + [28] r4 seat=1 Clash enemy miss dmg=0 php=62 ehp=224 roll=1/0 desc="fumble" + [29] r4 seat=0 Clash enemy hit dmg=7 php=111 ehp=224 roll=14/0 desc="" + [30] r4 seat=2 Clash enemy hit dmg=9 php=28 ehp=224 roll=13/0 desc="" + [31] r4 seat=2 Clash enemy hit dmg=8 php=20 ehp=224 roll=15/0 desc="" + [32] r4 seat=0 Clash enemy crit dmg=12 php=99 ehp=224 roll=20/0 desc="" + [33] r5 seat=1 Decisive player hit dmg=20 php=62 ehp=204 roll=13/0 desc="" + [34] r5 seat=0 Decisive enemy hit dmg=7 php=92 ehp=204 roll=9/0 desc="" + [35] r5 seat=2 Decisive enemy hit dmg=10 php=10 ehp=204 roll=5/0 desc="" + [36] r5 seat=0 Decisive enemy hit dmg=9 php=83 ehp=204 roll=12/0 desc="" + [37] r5 seat=0 Decisive enemy hit dmg=7 php=76 ehp=204 roll=17/0 desc="" + [38] r5 seat=2 Decisive enemy hit dmg=10 php=0 ehp=204 roll=7/0 desc="" + [39] r5 seat=0 Decisive player hit dmg=10 php=76 ehp=194 roll=8/0 desc="" + [40] r6 seat=1 Sudden Death player hit dmg=22 php=62 ehp=172 roll=16/0 desc="" + [41] r6 seat=0 Sudden Death player hit dmg=12 php=76 ehp=160 roll=4/0 desc="" + [42] r6 seat=1 Sudden Death enemy block dmg=5 php=57 ehp=160 roll=18/0 desc="" + [43] r6 seat=1 Sudden Death enemy hit dmg=11 php=46 ehp=160 roll=6/0 desc="" + [44] r6 seat=0 Sudden Death enemy hit dmg=8 php=68 ehp=160 roll=10/0 desc="" + [45] r7 seat=1 Sudden Death player hit dmg=21 php=46 ehp=139 roll=7/0 desc="" + [46] r7 seat=0 Sudden Death player hit dmg=13 php=68 ehp=126 roll=12/0 desc="" + [47] r7 seat=1 Sudden Death enemy hit dmg=11 php=35 ehp=126 roll=3/0 desc="" + [48] r7 seat=1 Sudden Death enemy crit dmg=8 php=27 ehp=126 roll=20/0 desc="" + [49] r8 seat=1 Sudden Death player hit dmg=19 php=27 ehp=107 roll=7/0 desc="" + [50] r8 seat=1 Sudden Death enemy hit dmg=10 php=17 ehp=107 roll=15/0 desc="" + [51] r8 seat=0 Sudden Death enemy hit dmg=9 php=59 ehp=107 roll=7/0 desc="" + [52] r8 seat=0 Sudden Death player hit dmg=12 php=59 ehp=95 roll=19/0 desc="" + [53] r9 seat=1 Sudden Death player hit dmg=22 php=17 ehp=73 roll=2/0 desc="" + [54] r9 seat=0 Sudden Death enemy hit dmg=9 php=50 ehp=73 roll=18/0 desc="" + [55] r9 seat=0 Sudden Death enemy hit dmg=9 php=41 ehp=73 roll=3/0 desc="" + [56] r9 seat=1 Sudden Death enemy hit dmg=10 php=7 ehp=73 roll=11/0 desc="" + [57] r9 seat=0 Sudden Death player hit dmg=15 php=41 ehp=58 roll=6/0 desc="" + [58] r10 seat=1 Sudden Death player hit dmg=23 php=7 ehp=35 roll=3/0 desc="" + [59] r10 seat=1 Sudden Death enemy hit dmg=11 php=0 ehp=35 roll=14/0 desc="" + [60] r10 seat=0 Sudden Death enemy hit dmg=8 php=33 ehp=35 roll=17/0 desc="" + [61] r10 seat=0 Sudden Death player hit dmg=12 php=33 ehp=23 roll=4/0 desc="" + [62] r10 seat=0 exhaust system timeout dmg=0 php=33 ehp=23 roll=0/0 desc="" + +=== trio/one weak seat seed=2 === +result: won=false timedOut=true rounds=8 survivors=true + enemy: start=345 entry=345 end=115 + seat[0]: start=160 entry=160 end=74 + seat[1]: start=90 entry=90 end=25 + seat[2]: start=70 entry=70 end=0 + [00] r1 seat=1 Opening player hit dmg=15 php=90 ehp=330 roll=16/0 desc="" + [01] r1 seat=2 Opening player hit dmg=5 php=70 ehp=325 roll=7/0 desc="" + [02] r1 seat=0 Opening enemy crit dmg=14 php=146 ehp=325 roll=20/0 desc="" + [03] r1 seat=0 Opening enemy hit dmg=6 php=140 ehp=325 roll=12/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=7 php=133 ehp=325 roll=18/0 desc="" + [05] r1 seat=2 Opening enemy hit dmg=7 php=63 ehp=325 roll=7/0 desc="" + [06] r1 seat=2 Opening enemy hit dmg=7 php=56 ehp=325 roll=2/0 desc="" + [07] r1 seat=0 Opening player hit dmg=10 php=133 ehp=315 roll=15/0 desc="" + [08] r2 seat=1 Clash player hit dmg=17 php=90 ehp=298 roll=3/0 desc="" + [09] r2 seat=0 Clash enemy hit dmg=8 php=125 ehp=298 roll=16/0 desc="" + [10] r2 seat=1 Clash enemy miss dmg=0 php=90 ehp=298 roll=1/0 desc="fumble" + [11] r2 seat=0 Clash enemy hit dmg=8 php=117 ehp=298 roll=9/0 desc="" + [12] r2 seat=2 Clash enemy hit dmg=10 php=46 ehp=298 roll=4/0 desc="" + [13] r2 seat=2 Clash enemy hit dmg=8 php=38 ehp=298 roll=15/0 desc="" + [14] r2 seat=2 Clash player miss dmg=0 php=38 ehp=298 roll=1/0 desc="fumble" + [15] r2 seat=0 Clash player miss dmg=0 php=117 ehp=298 roll=1/0 desc="fumble" + [16] r3 seat=0 Clash player hit dmg=11 php=117 ehp=287 roll=7/0 desc="" + [17] r3 seat=1 Clash player hit dmg=18 php=90 ehp=269 roll=5/0 desc="" + [18] r3 seat=0 Clash enemy hit dmg=6 php=111 ehp=269 roll=9/0 desc="" + [19] r3 seat=2 Clash enemy hit dmg=9 php=29 ehp=269 roll=18/0 desc="" + [20] r3 seat=1 Clash enemy hit dmg=7 php=83 ehp=269 roll=8/0 desc="" + [21] r3 seat=2 Clash enemy block dmg=4 php=25 ehp=269 roll=11/0 desc="" + [22] r3 seat=2 Clash enemy hit dmg=9 php=16 ehp=269 roll=10/0 desc="" + [23] r3 seat=2 Clash player hit dmg=6 php=16 ehp=263 roll=17/0 desc="" + [24] r4 seat=1 Decisive player hit dmg=19 php=83 ehp=244 roll=15/0 desc="" + [25] r4 seat=0 Decisive player hit dmg=10 php=111 ehp=234 roll=15/0 desc="" + [26] r4 seat=2 Decisive player hit dmg=5 php=16 ehp=229 roll=7/0 desc="" + [27] r4 seat=2 Decisive enemy hit dmg=10 php=6 ehp=229 roll=15/0 desc="" + [28] r4 seat=1 Decisive enemy hit dmg=9 php=74 ehp=229 roll=8/0 desc="" + [29] r4 seat=1 Decisive enemy hit dmg=8 php=66 ehp=229 roll=8/0 desc="" + [30] r4 seat=1 Decisive enemy hit dmg=8 php=58 ehp=229 roll=3/0 desc="" + [31] r4 seat=2 Decisive enemy hit dmg=7 php=0 ehp=229 roll=6/0 desc="" + [32] r5 seat=0 Sudden Death player hit dmg=11 php=111 ehp=218 roll=16/0 desc="" + [33] r5 seat=0 Sudden Death enemy hit dmg=8 php=103 ehp=218 roll=6/0 desc="" + [34] r5 seat=1 Sudden Death enemy block dmg=4 php=54 ehp=218 roll=7/0 desc="" + [35] r5 seat=1 Sudden Death player hit dmg=19 php=54 ehp=199 roll=19/0 desc="" + [36] r6 seat=1 Sudden Death player hit dmg=24 php=54 ehp=175 roll=5/0 desc="" + [37] r6 seat=0 Sudden Death player hit dmg=14 php=103 ehp=161 roll=9/0 desc="" + [38] r6 seat=1 Sudden Death enemy hit dmg=9 php=45 ehp=161 roll=17/0 desc="" + [39] r6 seat=1 Sudden Death enemy hit dmg=10 php=35 ehp=161 roll=15/0 desc="" + [40] r7 seat=1 Sudden Death player hit dmg=20 php=35 ehp=141 roll=3/0 desc="" + [41] r7 seat=0 Sudden Death player block dmg=5 php=103 ehp=136 roll=16/0 desc="" + [42] r7 seat=0 Sudden Death enemy hit dmg=10 php=93 ehp=136 roll=14/0 desc="" + [43] r7 seat=0 Sudden Death enemy hit dmg=10 php=83 ehp=136 roll=10/0 desc="" + [44] r8 seat=1 Sudden Death player hit dmg=21 php=35 ehp=115 roll=10/0 desc="" + [45] r8 seat=0 Sudden Death player miss dmg=0 php=83 ehp=115 roll=1/0 desc="fumble" + [46] r8 seat=0 Sudden Death enemy hit dmg=9 php=74 ehp=115 roll=8/0 desc="" + [47] r8 seat=1 Sudden Death enemy hit dmg=10 php=25 ehp=115 roll=14/0 desc="" + [48] r8 seat=0 exhaust system timeout dmg=0 php=74 ehp=115 roll=0/0 desc="" + +=== trio/one weak seat seed=3 === +result: won=true timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=18 + seat[0]: start=160 entry=160 end=52 + seat[1]: start=90 entry=90 end=0 + seat[2]: start=70 entry=70 end=0 + [00] r1 seat=1 Opening player hit dmg=15 php=90 ehp=330 roll=8/0 desc="" + [01] r1 seat=2 Opening player hit dmg=5 php=70 ehp=325 roll=18/0 desc="" + [02] r1 seat=0 Opening player hit dmg=9 php=160 ehp=316 roll=8/0 desc="" + [03] r1 seat=0 Opening enemy hit dmg=5 php=155 ehp=316 roll=16/0 desc="" + [04] r1 seat=1 Opening enemy hit dmg=7 php=83 ehp=316 roll=8/0 desc="" + [05] r1 seat=1 Opening enemy hit dmg=6 php=77 ehp=316 roll=15/0 desc="" + [06] r1 seat=0 Opening enemy hit dmg=6 php=149 ehp=316 roll=12/0 desc="" + [07] r1 seat=0 Opening enemy hit dmg=6 php=143 ehp=316 roll=13/0 desc="" + [08] r1 seat=1 Opening environment environmental dmg=6 php=71 ehp=316 roll=0/0 desc="" + [09] r2 seat=1 Clash player hit dmg=15 php=71 ehp=301 roll=4/0 desc="" + [10] r2 seat=2 Clash enemy hit dmg=8 php=62 ehp=301 roll=12/0 desc="" + [11] r2 seat=1 Clash enemy hit dmg=9 php=62 ehp=301 roll=14/0 desc="" + [12] r2 seat=1 Clash enemy block dmg=3 php=59 ehp=301 roll=17/0 desc="" + [13] r2 seat=0 Clash enemy hit dmg=8 php=135 ehp=301 roll=12/0 desc="" + [14] r2 seat=2 Clash enemy miss dmg=0 php=62 ehp=301 roll=1/0 desc="fumble" + [15] r2 seat=0 Clash player hit dmg=12 php=135 ehp=289 roll=7/0 desc="" + [16] r2 seat=2 Clash player hit dmg=5 php=62 ehp=284 roll=8/0 desc="" + [17] r3 seat=2 Clash player hit dmg=6 php=62 ehp=278 roll=19/0 desc="" + [18] r3 seat=1 Clash player hit dmg=19 php=59 ehp=259 roll=18/0 desc="" + [19] r3 seat=0 Clash player hit dmg=12 php=135 ehp=247 roll=11/0 desc="" + [20] r3 seat=2 Clash enemy hit dmg=8 php=54 ehp=247 roll=5/0 desc="" + [21] r3 seat=2 Clash enemy hit dmg=8 php=46 ehp=247 roll=7/0 desc="" + [22] r3 seat=0 Clash enemy hit dmg=8 php=127 ehp=247 roll=4/0 desc="" + [23] r3 seat=2 Clash enemy hit dmg=8 php=38 ehp=247 roll=16/0 desc="" + [24] r3 seat=2 Clash enemy hit dmg=8 php=30 ehp=247 roll=8/0 desc="" + [25] r3 seat=0 Clash environment environmental dmg=3 php=124 ehp=247 roll=0/0 desc="" + [26] r4 seat=1 Clash player hit dmg=17 php=59 ehp=230 roll=5/0 desc="" + [27] r4 seat=0 Clash player hit dmg=10 php=124 ehp=220 roll=14/0 desc="" + [28] r4 seat=2 Clash enemy hit dmg=8 php=22 ehp=220 roll=10/0 desc="" + [29] r4 seat=2 Clash enemy hit dmg=9 php=13 ehp=220 roll=4/0 desc="" + [30] r4 seat=1 Clash enemy hit dmg=9 php=50 ehp=220 roll=4/0 desc="" + [31] r4 seat=0 Clash enemy hit dmg=8 php=116 ehp=220 roll=12/0 desc="" + [32] r4 seat=0 Clash enemy hit dmg=8 php=108 ehp=220 roll=11/0 desc="" + [33] r4 seat=2 Clash player hit dmg=6 php=13 ehp=214 roll=3/0 desc="" + [34] r5 seat=1 Decisive player hit dmg=20 php=50 ehp=194 roll=5/0 desc="" + [35] r5 seat=0 Decisive player hit dmg=11 php=108 ehp=183 roll=10/0 desc="" + [36] r5 seat=2 Decisive player hit dmg=6 php=13 ehp=177 roll=17/0 desc="" + [37] r5 seat=2 Decisive enemy hit dmg=9 php=4 ehp=177 roll=17/0 desc="" + [38] r5 seat=2 Decisive enemy hit dmg=9 php=0 ehp=177 roll=13/0 desc="" + [39] r5 seat=1 Decisive enemy hit dmg=8 php=42 ehp=177 roll=8/0 desc="" + [40] r5 seat=0 Decisive enemy hit dmg=7 php=101 ehp=177 roll=6/0 desc="" + [41] r5 seat=0 Decisive enemy hit dmg=7 php=94 ehp=177 roll=16/0 desc="" + [42] r6 seat=1 Sudden Death player hit dmg=19 php=42 ehp=158 roll=10/0 desc="" + [43] r6 seat=1 Sudden Death enemy hit dmg=11 php=31 ehp=158 roll=14/0 desc="" + [44] r6 seat=1 Sudden Death enemy hit dmg=10 php=21 ehp=158 roll=18/0 desc="" + [45] r6 seat=0 Sudden Death player hit dmg=13 php=94 ehp=145 roll=9/0 desc="" + [46] r6 seat=0 Sudden Death environment environmental dmg=3 php=91 ehp=145 roll=0/0 desc="" + [47] r7 seat=1 Sudden Death player hit dmg=18 php=21 ehp=127 roll=4/0 desc="" + [48] r7 seat=0 Sudden Death player hit dmg=14 php=91 ehp=113 roll=3/0 desc="" + [49] r7 seat=1 Sudden Death enemy hit dmg=8 php=13 ehp=113 roll=8/0 desc="" + [50] r7 seat=0 Sudden Death enemy hit dmg=10 php=81 ehp=113 roll=12/0 desc="" + [51] r7 seat=0 Sudden Death enemy hit dmg=8 php=73 ehp=113 roll=9/0 desc="" + [52] r8 seat=1 Sudden Death player hit dmg=19 php=13 ehp=94 roll=5/0 desc="" + [53] r8 seat=0 Sudden Death player hit dmg=14 php=73 ehp=80 roll=12/0 desc="" + [54] r8 seat=0 Sudden Death enemy hit dmg=9 php=64 ehp=80 roll=19/0 desc="" + [55] r8 seat=0 Sudden Death enemy hit dmg=8 php=56 ehp=80 roll=9/0 desc="" + [56] r9 seat=1 Sudden Death player hit dmg=21 php=13 ehp=59 roll=15/0 desc="" + [57] r9 seat=1 Sudden Death enemy hit dmg=9 php=4 ehp=59 roll=16/0 desc="" + [58] r9 seat=1 Sudden Death enemy hit dmg=9 php=0 ehp=59 roll=17/0 desc="" + [59] r9 seat=0 Sudden Death player crit dmg=28 php=56 ehp=31 roll=20/0 desc="" + [60] r10 seat=0 Sudden Death enemy block dmg=4 php=52 ehp=31 roll=9/0 desc="" + [61] r10 seat=0 Sudden Death player hit dmg=13 php=52 ehp=18 roll=11/0 desc="" + [62] r10 seat=0 exhaust system timeout dmg=0 php=52 ehp=18 roll=0/0 desc="" + +=== trio/one weak seat seed=7 === +result: won=true timedOut=true rounds=10 survivors=true + enemy: start=345 entry=345 end=7 + seat[0]: start=160 entry=160 end=6 + seat[1]: start=90 entry=90 end=18 + seat[2]: start=70 entry=70 end=0 + [00] r1 seat=1 Opening player hit dmg=13 php=90 ehp=332 roll=6/0 desc="" + [01] r1 seat=2 Opening player hit dmg=5 php=70 ehp=327 roll=16/0 desc="" + [02] r1 seat=0 Opening player hit dmg=8 php=160 ehp=319 roll=16/0 desc="" + [03] r1 seat=2 Opening enemy block dmg=4 php=66 ehp=319 roll=11/0 desc="" + [04] r1 seat=2 Opening enemy hit dmg=6 php=60 ehp=319 roll=14/0 desc="" + [05] r1 seat=2 Opening enemy hit dmg=7 php=53 ehp=319 roll=13/0 desc="" + [06] r1 seat=2 Opening enemy hit dmg=7 php=46 ehp=319 roll=18/0 desc="" + [07] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=319 roll=9/0 desc="" + [08] r1 seat=1 Opening environment environmental dmg=3 php=87 ehp=319 roll=0/0 desc="" + [09] r2 seat=1 Clash player hit dmg=15 php=87 ehp=304 roll=17/0 desc="" + [10] r2 seat=2 Clash player hit dmg=5 php=46 ehp=299 roll=8/0 desc="" + [11] r2 seat=1 Clash enemy hit dmg=9 php=78 ehp=299 roll=9/0 desc="" + [12] r2 seat=0 Clash enemy hit dmg=7 php=147 ehp=299 roll=10/0 desc="" + [13] r2 seat=0 Clash enemy hit dmg=7 php=140 ehp=299 roll=6/0 desc="" + [14] r2 seat=1 Clash enemy hit dmg=8 php=70 ehp=299 roll=16/0 desc="" + [15] r2 seat=0 Clash enemy hit dmg=8 php=132 ehp=299 roll=11/0 desc="" + [16] r2 seat=0 Clash player hit dmg=10 php=132 ehp=289 roll=18/0 desc="" + [17] r3 seat=2 Clash player hit dmg=7 php=46 ehp=282 roll=13/0 desc="" + [18] r3 seat=2 Clash enemy hit dmg=7 php=39 ehp=282 roll=8/0 desc="" + [19] r3 seat=1 Clash enemy hit dmg=7 php=63 ehp=282 roll=8/0 desc="" + [20] r3 seat=2 Clash enemy hit dmg=7 php=32 ehp=282 roll=10/0 desc="" + [21] r3 seat=0 Clash enemy hit dmg=8 php=124 ehp=282 roll=3/0 desc="" + [22] r3 seat=2 Clash enemy hit dmg=8 php=24 ehp=282 roll=10/0 desc="" + [23] r3 seat=0 Clash player hit dmg=11 php=124 ehp=271 roll=4/0 desc="" + [24] r3 seat=1 Clash player hit dmg=15 php=63 ehp=256 roll=6/0 desc="" + [25] r4 seat=1 Clash player crit dmg=30 php=63 ehp=226 roll=20/0 desc="" + [26] r4 seat=2 Clash player hit dmg=6 php=24 ehp=220 roll=13/0 desc="" + [27] r4 seat=1 Clash enemy crit dmg=14 php=49 ehp=220 roll=20/0 desc="" + [28] r4 seat=2 Clash enemy hit dmg=7 php=17 ehp=220 roll=2/0 desc="" + [29] r4 seat=2 Clash enemy miss dmg=0 php=17 ehp=220 roll=1/0 desc="fumble" + [30] r4 seat=0 Clash enemy hit dmg=8 php=116 ehp=220 roll=10/0 desc="" + [31] r4 seat=2 Clash enemy hit dmg=7 php=10 ehp=220 roll=17/0 desc="" + [32] r4 seat=0 Clash player hit dmg=9 php=116 ehp=211 roll=8/0 desc="" + [33] r5 seat=1 Decisive player hit dmg=16 php=49 ehp=195 roll=18/0 desc="" + [34] r5 seat=0 Decisive player hit dmg=13 php=116 ehp=182 roll=4/0 desc="" + [35] r5 seat=2 Decisive player hit dmg=7 php=10 ehp=175 roll=8/0 desc="" + [36] r5 seat=0 Decisive enemy hit dmg=9 php=107 ehp=175 roll=3/0 desc="" + [37] r5 seat=1 Decisive enemy hit dmg=7 php=42 ehp=175 roll=14/0 desc="" + [38] r5 seat=2 Decisive enemy hit dmg=8 php=2 ehp=175 roll=17/0 desc="" + [39] r5 seat=2 Decisive enemy block dmg=4 php=0 ehp=175 roll=7/0 desc="" + [40] r5 seat=0 Decisive enemy hit dmg=8 php=99 ehp=175 roll=17/0 desc="" + [41] r6 seat=1 Sudden Death player hit dmg=18 php=42 ehp=157 roll=15/0 desc="" + [42] r6 seat=1 Sudden Death enemy hit dmg=10 php=32 ehp=157 roll=5/0 desc="" + [43] r6 seat=0 Sudden Death enemy hit dmg=8 php=91 ehp=157 roll=14/0 desc="" + [44] r6 seat=0 Sudden Death player hit dmg=11 php=91 ehp=146 roll=6/0 desc="" + [45] r7 seat=1 Sudden Death player crit dmg=46 php=32 ehp=100 roll=20/0 desc="" + [46] r7 seat=0 Sudden Death enemy hit dmg=10 php=81 ehp=100 roll=18/0 desc="" + [47] r7 seat=0 Sudden Death enemy crit dmg=20 php=61 ehp=100 roll=20/0 desc="" + [48] r7 seat=0 Sudden Death enemy hit dmg=9 php=52 ehp=100 roll=5/0 desc="" + [49] r7 seat=0 Sudden Death player hit dmg=12 php=52 ehp=88 roll=8/0 desc="" + [50] r8 seat=1 Sudden Death player hit dmg=18 php=32 ehp=70 roll=4/0 desc="" + [51] r8 seat=0 Sudden Death player hit dmg=11 php=52 ehp=59 roll=4/0 desc="" + [52] r8 seat=0 Sudden Death enemy hit dmg=8 php=44 ehp=59 roll=6/0 desc="" + [53] r8 seat=1 Sudden Death enemy block dmg=4 php=28 ehp=59 roll=17/0 desc="" + [54] r9 seat=1 Sudden Death player hit dmg=18 php=28 ehp=41 roll=9/0 desc="" + [55] r9 seat=0 Sudden Death player miss dmg=0 php=44 ehp=41 roll=1/0 desc="fumble" + [56] r9 seat=0 Sudden Death enemy hit dmg=10 php=34 ehp=41 roll=17/0 desc="" + [57] r9 seat=0 Sudden Death enemy hit dmg=10 php=24 ehp=41 roll=8/0 desc="" + [58] r10 seat=1 Sudden Death player hit dmg=19 php=28 ehp=22 roll=13/0 desc="" + [59] r10 seat=1 Sudden Death enemy hit dmg=10 php=18 ehp=22 roll=16/0 desc="" + [60] r10 seat=0 Sudden Death enemy hit dmg=10 php=14 ehp=22 roll=11/0 desc="" + [61] r10 seat=0 Sudden Death enemy hit dmg=8 php=6 ehp=22 roll=14/0 desc="" + [62] r10 seat=0 Sudden Death player hit dmg=15 php=6 ehp=7 roll=9/0 desc="" + [63] r10 seat=0 exhaust system timeout dmg=0 php=6 ehp=7 roll=0/0 desc="" + +=== trio/one weak seat seed=42 === +result: won=false timedOut=true rounds=8 survivors=true + enemy: start=345 entry=345 end=99 + seat[0]: start=160 entry=160 end=54 + seat[1]: start=90 entry=90 end=0 + seat[2]: start=70 entry=70 end=0 + [00] r1 seat=1 Opening player miss dmg=0 php=90 ehp=345 roll=1/0 desc="fumble" + [01] r1 seat=0 Opening player hit dmg=8 php=160 ehp=337 roll=17/0 desc="" + [02] r1 seat=2 Opening player hit dmg=5 php=70 ehp=332 roll=19/0 desc="" + [03] r1 seat=2 Opening enemy hit dmg=7 php=63 ehp=332 roll=18/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=7 php=153 ehp=332 roll=19/0 desc="" + [05] r1 seat=1 Opening enemy hit dmg=7 php=83 ehp=332 roll=2/0 desc="" + [06] r1 seat=1 Opening enemy hit dmg=7 php=76 ehp=332 roll=2/0 desc="" + [07] r1 seat=0 Opening enemy hit dmg=6 php=147 ehp=332 roll=2/0 desc="" + [08] r1 seat=2 Opening environment environmental dmg=6 php=57 ehp=332 roll=0/0 desc="" + [09] r2 seat=1 Clash player hit dmg=17 php=76 ehp=315 roll=10/0 desc="" + [10] r2 seat=0 Clash player hit dmg=10 php=147 ehp=305 roll=15/0 desc="" + [11] r2 seat=2 Clash player hit dmg=7 php=57 ehp=298 roll=16/0 desc="" + [12] r2 seat=0 Clash enemy block dmg=4 php=143 ehp=298 roll=5/0 desc="" + [13] r2 seat=1 Clash enemy hit dmg=7 php=69 ehp=298 roll=6/0 desc="" + [14] r2 seat=0 Clash enemy miss dmg=0 php=143 ehp=298 roll=1/0 desc="fumble" + [15] r2 seat=2 Clash enemy hit dmg=10 php=47 ehp=298 roll=15/0 desc="" + [16] r2 seat=1 Clash enemy hit dmg=7 php=62 ehp=298 roll=4/0 desc="" + [17] r3 seat=1 Clash player hit dmg=19 php=62 ehp=279 roll=6/0 desc="" + [18] r3 seat=0 Clash player hit dmg=10 php=143 ehp=269 roll=4/0 desc="" + [19] r3 seat=2 Clash player hit dmg=6 php=47 ehp=263 roll=15/0 desc="" + [20] r3 seat=2 Clash enemy hit dmg=8 php=39 ehp=263 roll=5/0 desc="" + [21] r3 seat=2 Clash enemy hit dmg=9 php=30 ehp=263 roll=10/0 desc="" + [22] r3 seat=1 Clash enemy miss dmg=0 php=62 ehp=263 roll=1/0 desc="fumble" + [23] r3 seat=0 Clash enemy hit dmg=7 php=136 ehp=263 roll=17/0 desc="" + [24] r3 seat=2 Clash enemy hit dmg=8 php=22 ehp=263 roll=15/0 desc="" + [25] r4 seat=1 Decisive player hit dmg=20 php=62 ehp=243 roll=4/0 desc="" + [26] r4 seat=2 Decisive player hit dmg=6 php=22 ehp=237 roll=17/0 desc="" + [27] r4 seat=0 Decisive enemy block dmg=3 php=133 ehp=237 roll=4/0 desc="" + [28] r4 seat=0 Decisive enemy hit dmg=7 php=126 ehp=237 roll=15/0 desc="" + [29] r4 seat=0 Decisive enemy hit dmg=8 php=118 ehp=237 roll=3/0 desc="" + [30] r4 seat=2 Decisive enemy hit dmg=8 php=14 ehp=237 roll=13/0 desc="" + [31] r4 seat=0 Decisive enemy hit dmg=8 php=110 ehp=237 roll=7/0 desc="" + [32] r4 seat=0 Decisive player hit dmg=10 php=110 ehp=227 roll=7/0 desc="" + [33] r5 seat=1 Sudden Death player hit dmg=21 php=62 ehp=206 roll=13/0 desc="" + [34] r5 seat=2 Sudden Death player hit dmg=6 php=14 ehp=200 roll=15/0 desc="" + [35] r5 seat=0 Sudden Death player hit dmg=12 php=110 ehp=188 roll=7/0 desc="" + [36] r5 seat=0 Sudden Death enemy hit dmg=8 php=102 ehp=188 roll=8/0 desc="" + [37] r5 seat=0 Sudden Death enemy hit dmg=8 php=94 ehp=188 roll=4/0 desc="" + [38] r5 seat=2 Sudden Death enemy miss dmg=0 php=14 ehp=188 roll=1/0 desc="fumble" + [39] r5 seat=1 Sudden Death enemy hit dmg=9 php=53 ehp=188 roll=3/0 desc="" + [40] r5 seat=2 Sudden Death enemy hit dmg=11 php=3 ehp=188 roll=16/0 desc="" + [41] r6 seat=1 Sudden Death player hit dmg=19 php=53 ehp=169 roll=16/0 desc="" + [42] r6 seat=0 Sudden Death player hit dmg=14 php=94 ehp=155 roll=6/0 desc="" + [43] r6 seat=2 Sudden Death player hit dmg=7 php=3 ehp=148 roll=7/0 desc="" + [44] r6 seat=1 Sudden Death enemy block dmg=5 php=48 ehp=148 roll=11/0 desc="" + [45] r6 seat=1 Sudden Death enemy hit dmg=11 php=37 ehp=148 roll=7/0 desc="" + [46] r6 seat=0 Sudden Death enemy block dmg=4 php=90 ehp=148 roll=4/0 desc="" + [47] r6 seat=2 Sudden Death enemy hit dmg=10 php=0 ehp=148 roll=4/0 desc="" + [48] r6 seat=1 Sudden Death enemy hit dmg=10 php=27 ehp=148 roll=9/0 desc="" + [49] r7 seat=1 Sudden Death player hit dmg=18 php=27 ehp=130 roll=14/0 desc="" + [50] r7 seat=0 Sudden Death player miss dmg=0 php=90 ehp=130 roll=1/0 desc="fumble" + [51] r7 seat=1 Sudden Death enemy hit dmg=10 php=17 ehp=130 roll=14/0 desc="" + [52] r7 seat=0 Sudden Death enemy crit dmg=20 php=70 ehp=130 roll=20/0 desc="" + [53] r7 seat=1 Sudden Death enemy hit dmg=8 php=9 ehp=130 roll=7/0 desc="" + [54] r8 seat=1 Sudden Death player hit dmg=18 php=9 ehp=112 roll=8/0 desc="" + [55] r8 seat=0 Sudden Death player hit dmg=13 php=70 ehp=99 roll=15/0 desc="" + [56] r8 seat=1 Sudden Death enemy hit dmg=10 php=0 ehp=99 roll=11/0 desc="" + [57] r8 seat=0 Sudden Death enemy crit dmg=16 php=54 ehp=99 roll=20/0 desc="" + [58] r8 seat=0 exhaust system timeout dmg=0 php=54 ehp=99 roll=0/0 desc="" + +=== trio/two glass seats seed=1 === +result: won=false timedOut=false rounds=7 survivors=false + enemy: start=172 entry=172 end=67 + seat[0]: start=160 entry=160 end=0 + seat[1]: start=12 entry=12 end=0 + seat[2]: start=12 entry=12 end=0 + [00] r1 seat=1 Opening player hit dmg=11 php=12 ehp=161 roll=11/0 desc="" + [01] r1 seat=2 Opening player hit dmg=12 php=12 ehp=149 roll=3/0 desc="" + [02] r1 seat=0 Opening player hit dmg=10 php=160 ehp=139 roll=11/0 desc="" + [03] r1 seat=2 Opening enemy hit dmg=20 php=0 ehp=139 roll=12/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=19 php=141 ehp=139 roll=2/0 desc="" + [05] r1 seat=1 Opening enemy hit dmg=23 php=0 ehp=139 roll=7/0 desc="" + [06] r1 seat=0 Opening enemy hit dmg=17 php=124 ehp=139 roll=8/0 desc="" + [07] r1 seat=0 Opening enemy hit dmg=16 php=108 ehp=139 roll=4/0 desc="" + [08] r2 seat=0 Clash player hit dmg=10 php=108 ehp=129 roll=8/0 desc="" + [09] r2 seat=0 Clash enemy hit dmg=20 php=88 ehp=129 roll=19/0 desc="" + [10] r3 seat=0 Clash enemy hit dmg=18 php=70 ehp=129 roll=5/0 desc="" + [11] r3 seat=0 Clash player hit dmg=11 php=70 ehp=118 roll=8/0 desc="" + [12] r4 seat=0 Clash enemy crit dmg=48 php=22 ehp=118 roll=20/0 desc="" + [13] r4 seat=0 Clash player hit dmg=12 php=22 ehp=106 roll=14/0 desc="" + [14] r5 seat=0 Decisive player hit dmg=13 php=22 ehp=93 roll=16/0 desc="" + [15] r5 seat=0 Decisive enemy hit dmg=21 php=1 ehp=93 roll=11/0 desc="" + [16] r6 seat=0 Sudden Death player hit dmg=13 php=1 ehp=80 roll=16/0 desc="" + [17] r6 seat=0 Sudden Death enemy miss dmg=0 php=1 ehp=80 roll=1/0 desc="fumble" + [18] r7 seat=0 Sudden Death player hit dmg=13 php=1 ehp=67 roll=11/0 desc="" + [19] r7 seat=0 Sudden Death enemy block dmg=12 php=0 ehp=67 roll=7/0 desc="" + +=== trio/two glass seats seed=2 === +result: won=false timedOut=false rounds=4 survivors=false + enemy: start=172 entry=172 end=80 + seat[0]: start=160 entry=160 end=0 + seat[1]: start=12 entry=12 end=0 + seat[2]: start=12 entry=12 end=0 + [00] r1 seat=1 Opening player hit dmg=14 php=12 ehp=158 roll=16/0 desc="" + [01] r1 seat=2 Opening player hit dmg=14 php=12 ehp=144 roll=7/0 desc="" + [02] r1 seat=0 Opening enemy crit dmg=40 php=120 ehp=144 roll=20/0 desc="" + [03] r1 seat=0 Opening enemy hit dmg=18 php=102 ehp=144 roll=12/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=20 php=82 ehp=144 roll=18/0 desc="" + [05] r1 seat=2 Opening enemy hit dmg=21 php=0 ehp=144 roll=7/0 desc="" + [06] r1 seat=0 Opening enemy hit dmg=19 php=63 ehp=144 roll=2/0 desc="" + [07] r1 seat=0 Opening player hit dmg=11 php=63 ehp=133 roll=15/0 desc="" + [08] r2 seat=1 Clash player hit dmg=14 php=12 ehp=119 roll=15/0 desc="" + [09] r2 seat=0 Clash player hit dmg=14 php=63 ehp=105 roll=12/0 desc="" + [10] r2 seat=0 Clash enemy hit dmg=20 php=43 ehp=105 roll=12/0 desc="" + [11] r2 seat=1 Clash enemy hit dmg=31 php=0 ehp=105 roll=13/0 desc="" + [12] r2 seat=0 Clash enemy hit dmg=20 php=23 ehp=105 roll=5/0 desc="" + [13] r3 seat=0 Clash enemy hit dmg=22 php=1 ehp=105 roll=6/0 desc="" + [14] r3 seat=0 Clash player hit dmg=11 php=1 ehp=94 roll=11/0 desc="" + [15] r4 seat=0 Decisive player hit dmg=14 php=1 ehp=80 roll=17/0 desc="" + [16] r4 seat=0 Decisive enemy hit dmg=25 php=0 ehp=80 roll=2/0 desc="" + +=== trio/two glass seats seed=3 === +result: won=false timedOut=false rounds=6 survivors=false + enemy: start=172 entry=172 end=66 + seat[0]: start=160 entry=160 end=0 + seat[1]: start=12 entry=12 end=0 + seat[2]: start=12 entry=12 end=0 + [00] r1 seat=2 Opening player hit dmg=14 php=12 ehp=158 roll=8/0 desc="" + [01] r1 seat=1 Opening player hit dmg=12 php=12 ehp=146 roll=18/0 desc="" + [02] r1 seat=0 Opening player hit dmg=11 php=160 ehp=135 roll=8/0 desc="" + [03] r1 seat=0 Opening enemy hit dmg=16 php=144 ehp=135 roll=16/0 desc="" + [04] r1 seat=1 Opening enemy hit dmg=23 php=0 ehp=135 roll=8/0 desc="" + [05] r1 seat=0 Opening enemy hit dmg=16 php=128 ehp=135 roll=15/0 desc="" + [06] r1 seat=0 Opening enemy hit dmg=17 php=111 ehp=135 roll=12/0 desc="" + [07] r1 seat=0 Opening enemy hit dmg=18 php=93 ehp=135 roll=13/0 desc="" + [08] r1 seat=2 Opening environment environmental dmg=6 php=6 ehp=135 roll=0/0 desc="" + [09] r2 seat=2 Clash player hit dmg=14 php=6 ehp=121 roll=4/0 desc="" + [10] r2 seat=0 Clash player crit dmg=28 php=93 ehp=93 roll=20/0 desc="" + [11] r2 seat=2 Clash enemy hit dmg=25 php=0 ehp=93 roll=14/0 desc="" + [12] r2 seat=0 Clash enemy hit dmg=18 php=75 ehp=93 roll=9/0 desc="" + [13] r2 seat=0 Clash environment environmental dmg=2 php=73 ehp=93 roll=0/0 desc="" + [14] r3 seat=0 Clash player hit dmg=14 php=73 ehp=79 roll=7/0 desc="" + [15] r3 seat=0 Clash enemy hit dmg=18 php=55 ehp=79 roll=8/0 desc="" + [16] r4 seat=0 Decisive player hit dmg=13 php=55 ehp=66 roll=9/0 desc="" + [17] r4 seat=0 Decisive enemy hit dmg=22 php=33 ehp=66 roll=8/0 desc="" + [18] r5 seat=0 Sudden Death player miss dmg=0 php=33 ehp=66 roll=1/0 desc="fumble" + [19] r5 seat=0 Sudden Death enemy hit dmg=29 php=4 ehp=66 roll=15/0 desc="" + [20] r6 seat=0 Sudden Death enemy hit dmg=24 php=0 ehp=66 roll=16/0 desc="" + +=== trio/two glass seats seed=7 === +result: won=false timedOut=false rounds=6 survivors=false + enemy: start=172 entry=172 end=97 + seat[0]: start=160 entry=160 end=0 + seat[1]: start=12 entry=12 end=0 + seat[2]: start=12 entry=12 end=0 + [00] r1 seat=2 Opening player hit dmg=12 php=12 ehp=160 roll=6/0 desc="" + [01] r1 seat=1 Opening player hit dmg=14 php=12 ehp=146 roll=16/0 desc="" + [02] r1 seat=0 Opening player hit dmg=9 php=160 ehp=137 roll=16/0 desc="" + [03] r1 seat=2 Opening enemy block dmg=12 php=0 ehp=137 roll=11/0 desc="" + [04] r1 seat=1 Opening enemy hit dmg=19 php=0 ehp=137 roll=14/0 desc="" + [05] r1 seat=0 Opening enemy hit dmg=19 php=141 ehp=137 roll=13/0 desc="" + [06] r1 seat=0 Opening enemy hit dmg=19 php=122 ehp=137 roll=18/0 desc="" + [07] r1 seat=0 Opening enemy hit dmg=17 php=105 ehp=137 roll=9/0 desc="" + [08] r2 seat=0 Clash player hit dmg=13 php=105 ehp=124 roll=11/0 desc="" + [09] r2 seat=0 Clash enemy hit dmg=19 php=86 ehp=124 roll=17/0 desc="" + [10] r3 seat=0 Clash player hit dmg=14 php=86 ehp=110 roll=3/0 desc="" + [11] r3 seat=0 Clash enemy hit dmg=24 php=62 ehp=110 roll=16/0 desc="" + [12] r4 seat=0 Clash player miss dmg=0 php=62 ehp=110 roll=1/0 desc="fumble" + [13] r4 seat=0 Clash enemy hit dmg=23 php=39 ehp=110 roll=11/0 desc="" + [14] r5 seat=0 Decisive player hit dmg=13 php=39 ehp=97 roll=18/0 desc="" + [15] r5 seat=0 Decisive enemy block dmg=13 php=26 ehp=97 roll=5/0 desc="" + [16] r6 seat=0 Sudden Death enemy hit dmg=26 php=0 ehp=97 roll=4/0 desc="" + +=== trio/two glass seats seed=42 === +result: won=false timedOut=false rounds=6 survivors=false + enemy: start=172 entry=172 end=107 + seat[0]: start=160 entry=160 end=0 + seat[1]: start=12 entry=12 end=0 + seat[2]: start=12 entry=12 end=0 + [00] r1 seat=1 Opening player miss dmg=0 php=12 ehp=172 roll=1/0 desc="fumble" + [01] r1 seat=2 Opening player hit dmg=12 php=12 ehp=160 roll=17/0 desc="" + [02] r1 seat=0 Opening player hit dmg=9 php=160 ehp=151 roll=19/0 desc="" + [03] r1 seat=2 Opening enemy hit dmg=23 php=0 ehp=151 roll=18/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=19 php=141 ehp=151 roll=19/0 desc="" + [05] r1 seat=0 Opening enemy hit dmg=18 php=123 ehp=151 roll=2/0 desc="" + [06] r1 seat=1 Opening enemy hit dmg=22 php=0 ehp=151 roll=2/0 desc="" + [07] r1 seat=0 Opening enemy hit dmg=18 php=105 ehp=151 roll=2/0 desc="" + [08] r2 seat=0 Clash player miss dmg=0 php=105 ehp=151 roll=1/0 desc="fumble" + [09] r2 seat=0 Clash enemy hit dmg=20 php=85 ehp=151 roll=16/0 desc="" + [10] r3 seat=0 Clash player crit dmg=24 php=85 ehp=127 roll=20/0 desc="" + [11] r3 seat=0 Clash enemy hit dmg=18 php=67 ehp=127 roll=7/0 desc="" + [12] r3 seat=0 Clash environment environmental dmg=4 php=63 ehp=127 roll=0/0 desc="" + [13] r4 seat=0 Decisive player block dmg=6 php=63 ehp=121 roll=14/0 desc="" + [14] r4 seat=0 Decisive enemy hit dmg=20 php=43 ehp=121 roll=4/0 desc="" + [15] r5 seat=0 Sudden Death player miss dmg=0 php=43 ehp=121 roll=1/0 desc="fumble" + [16] r5 seat=0 Sudden Death enemy hit dmg=29 php=14 ehp=121 roll=6/0 desc="" + [17] r6 seat=0 Sudden Death player hit dmg=14 php=14 ehp=107 roll=5/0 desc="" + [18] r6 seat=0 Sudden Death enemy crit dmg=52 php=0 ehp=107 roll=20/0 desc="" + +=== duo/vs ability enemy seed=1 === +result: won=true timedOut=false rounds=5 survivors=true + enemy: start=138 entry=138 end=0 + seat[0]: start=160 entry=160 end=122 + seat[1]: start=90 entry=90 end=44 + [00] r1 seat=1 Opening player hit dmg=18 php=90 ehp=120 roll=5/0 desc="" + [01] r1 seat=0 Opening player hit dmg=10 php=160 ehp=110 roll=5/0 desc="" + [02] r1 seat=1 Opening enemy hit dmg=7 php=83 ehp=110 roll=11/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=6 php=77 ehp=110 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=110 roll=19/0 desc="" + [05] r1 seat=0 Opening environment environmental dmg=5 php=149 ehp=110 roll=0/0 desc="" + [06] r2 seat=1 Clash player hit dmg=18 php=77 ehp=92 roll=4/0 desc="" + [07] r2 seat=1 Clash enemy hit dmg=9 php=68 ehp=92 roll=13/0 desc="" + [08] r2 seat=0 Clash enemy hit dmg=6 php=143 ehp=92 roll=12/0 desc="" + [09] r2 seat=0 Clash player hit dmg=13 php=143 ehp=79 roll=4/0 desc="" + [10] r3 seat=0 Clash player hit dmg=12 php=143 ehp=67 roll=9/0 desc="" + [11] r3 seat=1 Clash player hit dmg=22 php=68 ehp=45 roll=6/0 desc="" + [12] r3 seat=0 Clash enemy hit dmg=8 php=135 ehp=45 roll=17/0 desc="" + [13] r3 seat=1 Clash enemy hit dmg=8 php=60 ehp=45 roll=5/0 desc="" + [14] r3 seat=0 Clash enemy hit dmg=8 php=127 ehp=45 roll=7/0 desc="" + [15] r4 seat=1 Clash player hit dmg=19 php=60 ehp=26 roll=3/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=9 php=51 ehp=26 roll=11/0 desc="" + [17] r4 seat=1 Clash enemy hit dmg=7 php=44 ehp=26 roll=8/0 desc="" + [18] r4 seat=0 Clash player hit dmg=13 php=127 ehp=13 roll=9/0 desc="" + [19] r4 seat=0 Clash environment environmental dmg=5 php=122 ehp=13 roll=0/0 desc="" + [20] r5 seat=1 Decisive player hit dmg=22 php=44 ehp=0 roll=11/0 desc="" + +=== duo/vs ability enemy seed=2 === +result: won=true timedOut=false rounds=5 survivors=true + enemy: start=138 entry=138 end=0 + seat[0]: start=160 entry=160 end=108 + seat[1]: start=90 entry=90 end=56 + [00] r1 seat=1 Opening player hit dmg=18 php=90 ehp=120 roll=12/0 desc="" + [01] r1 seat=0 Opening player hit dmg=10 php=160 ehp=110 roll=3/0 desc="" + [02] r1 seat=0 Opening enemy crit dmg=14 php=146 ehp=110 roll=20/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=7 php=83 ehp=110 roll=12/0 desc="" + [04] r2 seat=1 Clash player hit dmg=19 php=83 ehp=91 roll=5/0 desc="" + [05] r2 seat=0 Clash player hit dmg=12 php=146 ehp=79 roll=3/0 desc="" + [06] r2 seat=0 Clash enemy hit dmg=8 php=138 ehp=79 roll=18/0 desc="" + [07] r2 seat=0 Clash enemy hit dmg=8 php=130 ehp=79 roll=10/0 desc="" + [08] r3 seat=1 Clash player hit dmg=21 php=83 ehp=58 roll=16/0 desc="" + [09] r3 seat=0 Clash enemy miss dmg=0 php=130 ehp=58 roll=1/0 desc="fumble" + [10] r3 seat=0 Clash enemy hit dmg=8 php=122 ehp=58 roll=9/0 desc="" + [11] r3 seat=0 Clash player hit dmg=12 php=122 ehp=46 roll=16/0 desc="" + [12] r4 seat=1 Clash player miss dmg=0 php=83 ehp=46 roll=1/0 desc="fumble" + [13] r4 seat=0 Clash player hit dmg=11 php=122 ehp=35 roll=19/0 desc="" + [14] r4 seat=0 Clash enemy hit dmg=6 php=116 ehp=35 roll=11/0 desc="" + [15] r4 seat=1 Clash enemy hit dmg=9 php=74 ehp=35 roll=7/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=9 php=65 ehp=35 roll=4/0 desc="" + [17] r5 seat=1 Decisive player hit dmg=23 php=65 ehp=12 roll=9/0 desc="" + [18] r5 seat=1 Decisive enemy hit dmg=9 php=56 ehp=12 roll=19/0 desc="" + [19] r5 seat=0 Decisive enemy hit dmg=8 php=108 ehp=12 roll=18/0 desc="" + [20] r5 seat=0 Decisive player hit dmg=13 php=108 ehp=0 roll=8/0 desc="" + +=== duo/vs ability enemy seed=3 === +result: won=true timedOut=false rounds=5 survivors=true + enemy: start=138 entry=138 end=0 + seat[0]: start=160 entry=160 end=116 + seat[1]: start=90 entry=90 end=67 + [00] r1 seat=1 Opening player hit dmg=15 php=90 ehp=123 roll=11/0 desc="" + [01] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=123 roll=18/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=6 php=148 ehp=123 roll=10/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=8 php=82 ehp=123 roll=13/0 desc="" + [04] r1 seat=0 Opening player hit dmg=9 php=148 ehp=114 roll=5/0 desc="" + [05] r2 seat=1 Clash player hit dmg=21 php=82 ehp=93 roll=2/0 desc="" + [06] r2 seat=0 Clash player hit dmg=13 php=148 ehp=80 roll=19/0 desc="" + [07] r2 seat=0 Clash enemy hit dmg=8 php=140 ehp=80 roll=2/0 desc="" + [08] r2 seat=0 Clash enemy hit dmg=6 php=134 ehp=80 roll=6/0 desc="" + [09] r2 seat=1 Clash environment environmental dmg=6 php=76 ehp=80 roll=0/0 desc="" + [10] r3 seat=1 Clash player hit dmg=21 php=76 ehp=59 roll=14/0 desc="" + [11] r3 seat=0 Clash enemy block dmg=3 php=131 ehp=59 roll=17/0 desc="" + [12] r3 seat=0 Clash enemy hit dmg=8 php=123 ehp=59 roll=12/0 desc="" + [13] r3 seat=0 Clash player hit dmg=12 php=123 ehp=47 roll=18/0 desc="" + [14] r4 seat=1 Clash player hit dmg=20 php=76 ehp=27 roll=19/0 desc="" + [15] r4 seat=1 Clash enemy hit dmg=9 php=67 ehp=27 roll=13/0 desc="" + [16] r4 seat=0 Clash enemy hit dmg=7 php=116 ehp=27 roll=8/0 desc="" + [17] r4 seat=0 Clash player hit dmg=13 php=116 ehp=14 roll=5/0 desc="" + [18] r5 seat=1 Decisive player hit dmg=19 php=67 ehp=0 roll=7/0 desc="" + +=== duo/vs ability enemy seed=7 === +result: won=true timedOut=false rounds=5 survivors=true + enemy: start=138 entry=138 end=0 + seat[0]: start=160 entry=160 end=116 + seat[1]: start=90 entry=90 end=62 + [00] r1 seat=1 Opening player hit dmg=17 php=90 ehp=121 roll=10/0 desc="" + [01] r1 seat=0 Opening player hit dmg=11 php=160 ehp=110 roll=3/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=5 php=155 ehp=110 roll=16/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=6 php=84 ehp=110 roll=19/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=149 ehp=110 roll=11/0 desc="" + [05] r2 seat=1 Clash player hit dmg=18 php=84 ehp=92 roll=6/0 desc="" + [06] r2 seat=0 Clash player hit dmg=12 php=149 ehp=80 roll=18/0 desc="" + [07] r2 seat=1 Clash enemy hit dmg=8 php=76 ehp=80 roll=4/0 desc="" + [08] r2 seat=0 Clash enemy hit dmg=7 php=142 ehp=80 roll=11/0 desc="" + [09] r2 seat=0 Clash enemy hit dmg=8 php=134 ehp=80 roll=13/0 desc="" + [10] r2 seat=0 Clash environment environmental dmg=5 php=129 ehp=80 roll=0/0 desc="" + [11] r3 seat=1 Clash player hit dmg=17 php=76 ehp=63 roll=7/0 desc="" + [12] r3 seat=0 Clash enemy hit dmg=7 php=122 ehp=63 roll=2/0 desc="" + [13] r3 seat=0 Clash enemy hit dmg=6 php=116 ehp=63 roll=15/0 desc="" + [14] r3 seat=0 Clash player hit dmg=13 php=116 ehp=50 roll=11/0 desc="" + [15] r4 seat=1 Clash player hit dmg=20 php=76 ehp=30 roll=18/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=7 php=69 ehp=30 roll=4/0 desc="" + [17] r4 seat=1 Clash enemy hit dmg=7 php=62 ehp=30 roll=8/0 desc="" + [18] r4 seat=0 Clash player block dmg=6 php=116 ehp=24 roll=10/0 desc="" + [19] r5 seat=1 Decisive player hit dmg=22 php=62 ehp=2 roll=3/0 desc="" + [20] r5 seat=0 Decisive player hit dmg=13 php=116 ehp=0 roll=17/0 desc="" + +=== duo/vs ability enemy seed=42 === +result: won=true timedOut=false rounds=5 survivors=true + enemy: start=138 entry=138 end=0 + seat[0]: start=160 entry=160 end=125 + seat[1]: start=90 entry=90 end=59 + [00] r1 seat=1 Opening player hit dmg=14 php=90 ehp=124 roll=13/0 desc="" + [01] r1 seat=0 Opening player hit dmg=9 php=160 ehp=115 roll=5/0 desc="" + [02] r1 seat=0 Opening enemy hit dmg=6 php=154 ehp=115 roll=15/0 desc="" + [03] r1 seat=1 Opening enemy hit dmg=7 php=83 ehp=115 roll=6/0 desc="" + [04] r1 seat=0 Opening enemy hit dmg=6 php=148 ehp=115 roll=13/0 desc="" + [05] r2 seat=1 Clash player hit dmg=22 php=83 ehp=93 roll=6/0 desc="" + [06] r2 seat=0 Clash player hit dmg=10 php=148 ehp=83 roll=12/0 desc="" + [07] r2 seat=0 Clash enemy hit dmg=7 php=141 ehp=83 roll=3/0 desc="" + [08] r2 seat=1 Clash enemy hit dmg=7 php=76 ehp=83 roll=5/0 desc="" + [09] r2 seat=0 Clash enemy block dmg=3 php=138 ehp=83 roll=15/0 desc="" + [10] r3 seat=1 Clash player hit dmg=17 php=76 ehp=66 roll=13/0 desc="" + [11] r3 seat=0 Clash player hit dmg=14 php=138 ehp=52 roll=12/0 desc="" + [12] r3 seat=1 Clash enemy hit dmg=9 php=67 ehp=52 roll=15/0 desc="" + [13] r3 seat=0 Clash enemy hit dmg=6 php=132 ehp=52 roll=4/0 desc="" + [14] r3 seat=0 Clash enemy hit dmg=7 php=125 ehp=52 roll=9/0 desc="" + [15] r4 seat=1 Clash player hit dmg=20 php=67 ehp=32 roll=10/0 desc="" + [16] r4 seat=1 Clash enemy hit dmg=8 php=59 ehp=32 roll=15/0 desc="" + [17] r4 seat=1 Clash enemy miss dmg=0 php=59 ehp=32 roll=1/0 desc="fumble" + [18] r4 seat=0 Clash player hit dmg=13 php=125 ehp=19 roll=9/0 desc="" + [19] r5 seat=1 Decisive player hit dmg=19 php=59 ehp=0 roll=3/0 desc="" + +=== solo/one-seat roster seed=1 === +result: won=true timedOut=true rounds=8 survivors=true + enemy: start=60 entry=60 end=1 + seat[0]: start=100 entry=100 end=23 + [00] r1 seat=0 Opening enemy hit dmg=6 php=94 ehp=60 roll=9/0 desc="" + [01] r1 seat=0 Opening player hit dmg=9 php=94 ehp=51 roll=11/0 desc="" + [02] r2 seat=0 Clash player hit dmg=13 php=94 ehp=38 roll=8/0 desc="" + [03] r2 seat=0 Clash enemy hit dmg=7 php=87 ehp=38 roll=6/0 desc="" + [04] r3 seat=0 Clash player hit dmg=11 php=87 ehp=27 roll=6/0 desc="" + [05] r3 seat=0 Clash enemy hit dmg=8 php=79 ehp=27 roll=12/0 desc="" + [06] r4 seat=0 Decisive enemy hit dmg=8 php=71 ehp=27 roll=4/0 desc="" + [07] r4 seat=0 Decisive player hit dmg=13 php=71 ehp=14 roll=2/0 desc="" + [08] r5 seat=0 Sudden Death player hit dmg=13 php=71 ehp=1 roll=8/0 desc="" + [09] r5 seat=0 Sudden Death enemy hit dmg=9 php=62 ehp=1 roll=19/0 desc="" + [10] r6 seat=0 Sudden Death player miss dmg=0 php=62 ehp=1 roll=1/0 desc="fumble" + [11] r6 seat=0 Sudden Death enemy hit dmg=9 php=53 ehp=1 roll=9/0 desc="" + [12] r7 seat=0 Sudden Death player miss dmg=0 php=53 ehp=1 roll=1/0 desc="fumble" + [13] r7 seat=0 Sudden Death enemy hit dmg=10 php=43 ehp=1 roll=17/0 desc="" + [14] r8 seat=0 Sudden Death player miss dmg=0 php=43 ehp=1 roll=1/0 desc="fumble" + [15] r8 seat=0 Sudden Death enemy crit dmg=20 php=23 ehp=1 roll=20/0 desc="" + [16] r8 seat=0 exhaust system timeout dmg=0 php=23 ehp=1 roll=0/0 desc="" + +=== solo/one-seat roster seed=2 === +result: won=true timedOut=false rounds=6 survivors=true + enemy: start=60 entry=60 end=0 + seat[0]: start=100 entry=100 end=54 + [00] r1 seat=0 Opening player hit dmg=10 php=100 ehp=50 roll=16/0 desc="" + [01] r1 seat=0 Opening enemy hit dmg=7 php=93 ehp=50 roll=16/0 desc="" + [02] r2 seat=0 Clash player hit dmg=12 php=93 ehp=38 roll=16/0 desc="" + [03] r2 seat=0 Clash enemy hit dmg=8 php=85 ehp=38 roll=12/0 desc="" + [04] r3 seat=0 Clash enemy crit dmg=16 php=69 ehp=38 roll=20/0 desc="" + [05] r3 seat=0 Clash player hit dmg=13 php=69 ehp=25 roll=4/0 desc="" + [06] r4 seat=0 Clash player hit dmg=15 php=69 ehp=10 roll=15/0 desc="" + [07] r4 seat=0 Clash enemy hit dmg=9 php=60 ehp=10 roll=6/0 desc="" + [08] r4 seat=0 Clash environment environmental dmg=2 php=58 ehp=10 roll=0/0 desc="" + [09] r5 seat=0 Decisive enemy block dmg=4 php=54 ehp=10 roll=14/0 desc="" + [10] r5 seat=0 Decisive player miss dmg=0 php=54 ehp=10 roll=1/0 desc="fumble" + [11] r6 seat=0 Sudden Death player hit dmg=14 php=54 ehp=0 roll=12/0 desc="" + +=== solo/one-seat roster seed=3 === +result: won=true timedOut=false rounds=5 survivors=true + enemy: start=60 entry=60 end=0 + seat[0]: start=100 entry=100 end=70 + [00] r1 seat=0 Opening player block dmg=6 php=100 ehp=54 roll=5/0 desc="" + [01] r1 seat=0 Opening enemy hit dmg=7 php=93 ehp=54 roll=8/0 desc="" + [02] r2 seat=0 Clash player hit dmg=14 php=93 ehp=40 roll=10/0 desc="" + [03] r2 seat=0 Clash enemy hit dmg=7 php=86 ehp=40 roll=14/0 desc="" + [04] r3 seat=0 Clash player hit dmg=13 php=86 ehp=27 roll=9/0 desc="" + [05] r3 seat=0 Clash enemy hit dmg=9 php=77 ehp=27 roll=18/0 desc="" + [06] r4 seat=0 Decisive player hit dmg=14 php=77 ehp=13 roll=13/0 desc="" + [07] r4 seat=0 Decisive enemy hit dmg=7 php=70 ehp=13 roll=18/0 desc="" + [08] r5 seat=0 Sudden Death player hit dmg=13 php=70 ehp=0 roll=6/0 desc="" + +=== solo/one-seat roster seed=7 === +result: won=true timedOut=false rounds=5 survivors=true + enemy: start=60 entry=60 end=0 + seat[0]: start=100 entry=100 end=69 + [00] r1 seat=0 Opening enemy miss dmg=0 php=100 ehp=60 roll=1/0 desc="fumble" + [01] r1 seat=0 Opening player hit dmg=11 php=100 ehp=49 roll=11/0 desc="" + [02] r2 seat=0 Clash player hit dmg=13 php=100 ehp=36 roll=15/0 desc="" + [03] r2 seat=0 Clash enemy hit dmg=9 php=91 ehp=36 roll=7/0 desc="" + [04] r2 seat=0 Clash environment environmental dmg=5 php=86 ehp=36 roll=0/0 desc="" + [05] r3 seat=0 Clash player hit dmg=13 php=86 ehp=23 roll=4/0 desc="" + [06] r3 seat=0 Clash enemy hit dmg=9 php=77 ehp=23 roll=6/0 desc="" + [07] r4 seat=0 Clash player hit dmg=12 php=77 ehp=11 roll=6/0 desc="" + [08] r4 seat=0 Clash enemy hit dmg=8 php=69 ehp=11 roll=18/0 desc="" + [09] r5 seat=0 Decisive player hit dmg=13 php=69 ehp=0 roll=8/0 desc="" + +=== solo/one-seat roster seed=42 === +result: won=true timedOut=false rounds=7 survivors=true + enemy: start=60 entry=60 end=0 + seat[0]: start=100 entry=100 end=45 + [00] r1 seat=0 Opening enemy hit dmg=6 php=94 ehp=60 roll=11/0 desc="" + [01] r1 seat=0 Opening player miss dmg=0 php=94 ehp=60 roll=1/0 desc="fumble" + [02] r1 seat=0 Opening environment environmental dmg=3 php=91 ehp=60 roll=0/0 desc="" + [03] r2 seat=0 Clash player hit dmg=12 php=91 ehp=48 roll=15/0 desc="" + [04] r2 seat=0 Clash enemy hit dmg=8 php=83 ehp=48 roll=9/0 desc="" + [05] r3 seat=0 Clash player hit dmg=14 php=83 ehp=34 roll=2/0 desc="" + [06] r3 seat=0 Clash enemy hit dmg=8 php=75 ehp=34 roll=9/0 desc="" + [07] r4 seat=0 Clash player hit dmg=15 php=75 ehp=19 roll=6/0 desc="" + [08] r4 seat=0 Clash enemy hit dmg=7 php=68 ehp=19 roll=12/0 desc="" + [09] r5 seat=0 Decisive player miss dmg=0 php=68 ehp=19 roll=1/0 desc="fumble" + [10] r5 seat=0 Decisive enemy hit dmg=8 php=60 ehp=19 roll=16/0 desc="" + [11] r6 seat=0 Sudden Death player hit dmg=14 php=60 ehp=5 roll=15/0 desc="" + [12] r6 seat=0 Sudden Death enemy hit dmg=10 php=50 ehp=5 roll=14/0 desc="" + [13] r6 seat=0 Sudden Death environment environmental dmg=5 php=45 ehp=5 roll=0/0 desc="" + [14] r7 seat=0 Sudden Death player hit dmg=17 php=45 ehp=0 roll=12/0 desc="" + diff --git a/internal/plugin/zone_combat_party.go b/internal/plugin/zone_combat_party.go index 4875019..ffd5a2d 100644 --- a/internal/plugin/zone_combat_party.go +++ b/internal/plugin/zone_combat_party.go @@ -51,6 +51,11 @@ func (p *AdventurePlugin) runZoneCombatRoster( advChar *AdventureCharacter equip map[EquipmentSlot]*AdvEquipment mods CombatModifiers + // companion marks the hired NPC seat: it fights, but it owns none of the + // character-scoped effects the close-out loop applies, and its dndChar / + // advChar / equip are nil precisely so a missed guard panics loudly here + // rather than silently writing rows for a bot. + companion bool } var ( @@ -69,6 +74,22 @@ func (p *AdventurePlugin) runZoneCombatRoster( return PartyCombatResult{}, nil, err } + // The hired companion fights here too — the auto-resolve path is where + // most expedition rooms are actually decided. He is synthesized rather + // than loaded, and his seatBuild is flagged so the close-out loop below + // gives him no XP, no loot, and no post-combat persistence. + if isCompanionSeat(uid) { + class, level := companionLoadout(companionExpeditionFor(roster[0])) + player, e, _ := p.companionCombatant(class, level, monster, tier, dmMood) + if leader { + enemy = e + } + players = append(players, player) + builds = append(builds, seatBuild{uid: uid, mods: player.Mods, companion: true}) + seated = append(seated, uid) + continue + } + advChar, err := loadAdvCharacter(uid) if err != nil || advChar == nil { if leader { @@ -137,6 +158,14 @@ func (p *AdventurePlugin) runZoneCombatRoster( for i, b := range builds { seatRes := res.Seats[i] + // The companion swings and then goes back to filing copy: no inventory to + // deduct from, no sheet to persist to, no XP to earn. Every call below + // would either write rows for a bot or log an error about the rows it + // hasn't got. + if b.companion { + continue + } + // Remove the actual heal items consumed during combat (one inventory item // per heal_item event this seat fired). Cheapest-tier first. consumeFiredHealingItems(b.uid, countHealEventsFired(seatRes)) @@ -193,6 +222,12 @@ func (p *AdventurePlugin) closeOutZoneWin( ) (leaderDrop string, downed []id.UserID) { party := len(seated) > 1 for i, uid := range seated { + // The companion takes no cut and cannot die. He is not in the downed list + // either: that list is what the room narration mourns, and the party did + // not lose a friend when the hireling took a nap. + if isCompanionSeat(uid) { + continue + } if res.Seats[i].PlayerEndHP > 0 { drop := p.dropZoneLoot(uid, zone.ID, monster, isBoss, elite) if i == 0 { @@ -217,6 +252,12 @@ func (p *AdventurePlugin) closeOutZoneWin( // fight. func closeOutZoneLoss(res PartyCombatResult, seated []id.UserID, zone ZoneDefinition, deathSource string) (killed []id.UserID) { for i, uid := range seated { + // The companion is not killed and is not counted among the dead — he is + // not in the graveyard, and a wipe that lists him would have the news bot + // reporting its own funeral. + if isCompanionSeat(uid) { + continue + } if res.Seats[i].PlayerEndHP <= 0 { markAdventureDead(uid, deathSource, zone.Display) killed = append(killed, uid)